/* | |
* Created on 10-Nov-2005 TODO To change the template for this generated file go | |
* to Window - Preferences - Java - Code Style - Code Templates | |
*/ | |
package org.apache.test; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
/** | |
* @author hawkeye TODO To change the template for this generated type comment | |
* go to Window - Preferences - Java - Code Style - Code Templates | |
*/ | |
// We cache the entire output file in an array of char[] | |
public class Response | |
{ | |
private static final char CR ='\r'; | |
private static final char LF ='\n'; | |
public static final String CRLF =""+CR+LF; | |
public static final String DOUBLE_CRLF =CRLF+CRLF; | |
private static final String CONTENT_LENGTH ="Content-Length:"; | |
private char[] message; | |
private boolean chunked=false; | |
// whether the message has a "Connection: Close" http header in it. | |
private boolean hasCloseConnectionHeader=false; | |
public boolean hasCloseConnectionHeader( ) | |
{ | |
return hasCloseConnectionHeader; | |
} | |
/** | |
* Because the repositories we use accomodate the dos2unix conversion we | |
* have a problem. On unix the response files have the crlf converted to | |
* just cr. this method returns the crlf ! The HTTP header and chunk sizes | |
* need to be delimited with CR LF's. The 'server response' expected files | |
* used to mimic the response of a server when performing unit tests also | |
* need to have the correct delimiting character combinations in the fake | |
* response messages (generated by the monitor). Problems arise because the | |
* expected response files have been captured on different operating systems | |
* (Unix and Windows) and then edited using a variety of editors that | |
* interpret the CR character differently. Thus the file may be stored with | |
* or without the CR characters. Also, the souce control application can be | |
* set to convert a single LF character into a CR LF combination. The | |
* outcome of all this character manipulation is that LF's may or may not | |
* have the necessary associated CR for it to be recognised by the HTTP | |
* transport. It is not just sufficient to look for every LF and to prefix | |
* it with a CR because the context of that LF must be taken into | |
* consideration. For example, if a LF is found in the HTTP header then it | |
* definitely needs a CR prefix. This also applies when the message is | |
* chunked and the chunk size must also be delimited by the CR LF sequence. | |
* But, when a LF appears inside a chunk of data it should remain unchanged | |
* unless the chunk size is different from the actual data size between | |
* chunks, in which case the CR has probably been stripped out and needs to | |
* be replaced. Below is a low level design of what should work in all | |
* instances... Is the message chunked? NO: Convert all single LF sequences | |
* to CR+LF. NB: Because the value associated with the 'Content-Length' | |
* label is not used correctly, if the size of the file grows larger than | |
* the Content-Length value, no action is taken. Exit. YES: Convert all | |
* single LF sequences to CR+LF in the HTTP header. Set Index = location of | |
* the first 'chunk' value. Read the chunk size. While the chunk size > 0 | |
* Convert any single LF sequences to CR+LF that surround the chunk value. | |
* Is the character at offset Index + chunk size equal to CR or LF? NO: | |
* Count up the number of unaccompanied LF's in the chunk. Is the difference | |
* between the expected and actual chunk data length double the number of | |
* unaccompanied LF's? NO: Error - Irreconcilable differences between | |
* expected and actual chunk data sizes. Exit. YES: Convert all single LF | |
* sequences to CR+LF in the chunk data. Continue. Index = Index + chunk | |
* size. Read the new chunk size. End of while. Exit. | |
*/ | |
public Response(String message) | |
{ | |
this.message=message.toCharArray( ); | |
// parse the message to see if it has a Connection: Close in it | |
if (message.toLowerCase( ).indexOf("connection: close")>-1) | |
hasCloseConnectionHeader=true; | |
// Find out if the message is chunked | |
// simply look for the content length string | |
chunked= message.indexOf(CONTENT_LENGTH)==-1; | |
// emulate None OS machines when you want to test out the CRLF method | |
//emulateNoneWinmachines(); | |
// check and correct CRLF's | |
CheckAndCorrectCRLF( ); | |
} | |
/** | |
* This method is used when we are testing out this class. | |
* It converts ALL CRLF's into LF (as it might be on a none win machine) | |
*/ | |
private void emulateNoneWinmachines( ) | |
{ | |
String msgString = new String(getMessage()); | |
Pattern pattern=Pattern.compile(CRLF); | |
Matcher matcher=pattern.matcher(msgString); | |
if(matcher.find()) | |
msgString = matcher.replaceAll(""+LF); | |
message = msgString.toCharArray(); | |
} | |
public char[] getMessage( ) | |
{ | |
return message; | |
} | |
public String toString( ) | |
{ | |
return new String(message); | |
} | |
/** | |
* On platforms other than windows the CRLF's can get "helpfully" converted | |
* So, check the last two digits for CRLF - if they are LFLF then this is | |
* wrong and we will convert it to be correct. e.g. 0a 0a converts to 0d0a | |
* 0d0a | |
*/ | |
private void CheckAndCorrectCRLF( ) | |
{ | |
String modifiedResponse=""; | |
if (chunked) | |
{ | |
System.out.println("Information - Response is chunked."); | |
String messageString= new String(getMessage()); | |
modifiedResponse = correctHTTPHeaderSection(messageString); | |
if (messageString.indexOf("###") != -1) | |
modifiedResponse +=(correctChunkedSizes(getPostHTTPHeaderData(messageString))); | |
else | |
modifiedResponse +=(correctChunkedData(getPostHTTPHeaderData(messageString))); | |
message = modifiedResponse.toCharArray(); | |
} | |
else | |
{ | |
System.out.println("Information - Response is NOT chunked."); | |
if (System.getProperty("os.name").toLowerCase( ).startsWith("windows")) | |
System.out.println("Windows operating system - not converting crlf's"); | |
else | |
{ | |
String request=new String(getMessage( )); | |
modifiedResponse = correctHTTPHeaderSection(request); | |
message=modifiedResponse.toCharArray(); | |
} | |
} | |
} | |
/** | |
* @param message2 | |
* @return | |
*/ | |
private String getPostHTTPHeaderData(String response) | |
{ | |
Pattern pattern=Pattern.compile( "(^HTTP.*["+CRLF+"]*)(.*: .*["+CRLF+"]*)*(.*)"); | |
Matcher matcher = pattern.matcher(response); | |
if(!matcher.find()) | |
System.err.println( "ERROR: Response message does not contain a correctly formatted HTTP header:" +response); | |
return matcher.replaceAll("$3"); | |
} | |
private String correctChunkedSizes(String orgResponse) | |
{ | |
// If called, there must be hash's for chunksizes. | |
String hash = "###"; | |
String newResponse = ""; | |
// Remove hash line. | |
int hashPos = orgResponse.indexOf( hash ); | |
int endOfLinePos = orgResponse.indexOf('\n', hashPos); | |
orgResponse = orgResponse.substring( endOfLinePos + 1 ); | |
String chunk; | |
while( hashPos != -1 && orgResponse.length() > 0) | |
{ | |
// Find the next hash in the original response. | |
hashPos = orgResponse.indexOf( hash); | |
boolean eom = false; | |
if( hashPos == -1) | |
{ | |
hashPos = orgResponse.lastIndexOf( "0"); | |
eom = true; | |
} | |
// Ensure chunk ends with CRLF - if it does not, | |
// remove ending LF and replace with CRLF | |
chunk = orgResponse.substring( 0, hashPos); | |
if (!chunk.endsWith("\r\n")) | |
chunk = chunk.substring(0, chunk.length()-1) + "\r\n"; | |
// Add the next chunk length and data from the original to the new response. | |
newResponse += Integer.toHexString(chunk.length()-2) + "\r\n" + chunk; | |
// Remove the old chunk from the original response message. | |
if( eom) | |
{ | |
newResponse += "0\r\n\r\n"; | |
orgResponse = ""; | |
} | |
else | |
{ | |
endOfLinePos = orgResponse.indexOf('\n', hashPos); | |
orgResponse = orgResponse.substring( endOfLinePos + 1); | |
} | |
} | |
return newResponse; | |
} | |
/** | |
* @param string the response message - HTTPheaders | |
* @return correctly formatted (as best we can) http headers. | |
*/ | |
private String correctChunkedData(String request) | |
{ | |
// A chunk Is defined by | |
// <hexnumber><potentially CR><LF><any data><potentially CR><LF><hexnumber> | |
StringBuffer theCorrectedResponse = new StringBuffer(); | |
String theChunkStartPatternREGEX = "(\\p{XDigit}+)("+CR+"?"+LF+")"; | |
String theNextChunkSizeREGEX ="("+CR+"?"+LF+")"+theChunkStartPatternREGEX; | |
Pattern chunkStartPattern=Pattern.compile(theChunkStartPatternREGEX); | |
Pattern theNextChunkSizePattern = Pattern.compile(theNextChunkSizeREGEX); | |
Matcher chunkStartMatcher = chunkStartPattern.matcher(request); | |
Matcher theNextChunkSizeMatcher = theNextChunkSizePattern.matcher(request); | |
int chunkStart = 0; | |
int chunkEnd=0; | |
int chunkSize = 0; | |
int nextChunkSize = -1; | |
int nextChunkStart= -1; | |
int nextChunkEnd= -1; | |
String nextChunkSizeString=""; | |
while(chunkStartMatcher.find() && nextChunkSize!=0) | |
{ | |
String chunkSizeString=""; | |
if(nextChunkSize==-1) | |
{ | |
// means this is the first time round the loop so work it out | |
chunkSizeString = request.substring(chunkStartMatcher.start(), chunkStartMatcher.end()); | |
chunkSize = Integer.parseInt(chunkSizeString.trim(), 16); | |
chunkStart = chunkStartMatcher.end(); | |
// ensure the string has the correct crlf on it | |
chunkSizeString = chunkSizeString.trim()+CRLF; | |
} | |
else | |
{ | |
// we've already been round the loop and got the chunkSize | |
chunkSizeString = nextChunkSizeString; | |
chunkSize = nextChunkSize; | |
chunkStart = nextChunkStart; | |
// ensure the string has the correct crlf on it | |
// this one came with a different parsing algorithm so add on crlf to *both* ends ! | |
chunkSizeString = CRLF+chunkSizeString.trim()+CRLF; | |
} | |
// System.out.println( "chunkString = "+chunkSizeString.trim()); | |
// System.out.println( "Chunk size = "+chunkSize); | |
// System.out.println( "Chunked Start="+chunkStart); | |
// System.out.println( "chunkend = "+chunkEnd); | |
String chunkFromSize=null; | |
if(chunkStart+chunkSize>request.length()) | |
{ | |
System.err.println( "ChunksSize String is wrong"); | |
System.err.println( "chunksStart = "+chunkStart); | |
System.err.println( "ChunkSize = "+chunkSize); | |
System.err.println( "request.length = "+request.length()); | |
// We can work this out though it's probably the last or only chunk so just guess ! | |
// do this later on so we don't duplicate work> | |
// chunkFromSize is null so we can see later on that we need to guess | |
} | |
else | |
chunkFromSize = request.substring(chunkStart, chunkStart+chunkSize); | |
String chunk=""; | |
int endOfThisChunk; | |
// Now find the end of the chunk (and while we do that we actually find the next chunk size too ! | |
if(!theNextChunkSizeMatcher.find()) | |
endOfThisChunk = request.length()-1; | |
else | |
{ | |
chunkEnd = theNextChunkSizeMatcher.start(); | |
// get the nextchunkSize while we're here! | |
nextChunkSizeString = request.substring(theNextChunkSizeMatcher.start(), theNextChunkSizeMatcher.end()); | |
nextChunkStart = theNextChunkSizeMatcher.end(); | |
nextChunkSize = Integer.parseInt(nextChunkSizeString.trim(), 16); | |
endOfThisChunk = theNextChunkSizeMatcher.start(); | |
} | |
chunk = request.substring(chunkStart, endOfThisChunk); | |
if(chunkFromSize==null) | |
{ | |
// we need to set this value now we have probably worked out what it is | |
// this stops us from stopping but hopefully we won't fail too ! | |
chunkFromSize = chunk; | |
chunkSize=chunk.length(); | |
System.err.println( "Guessing the size is "+chunkFromSize.length()); | |
// we now also have to take into account the chunkSize and it's \r\n | |
// test it again to see whether we made it right or not | |
// make sure we set the chunksize string | |
if(!chunkSizeString.startsWith(""+CR)) | |
{ | |
// Then it's the beginning of the message | |
chunkSizeString = ""+Integer.toHexString(chunk.length())+CRLF; | |
} | |
else | |
chunkSizeString = CRLF+Integer.toHexString(chunk.length())+CRLF; | |
} | |
// Check that the chunk size really is the same size as it should be. | |
if(chunk.length() !=chunkSize) | |
{ | |
System.out.println( "Warning: Chunksize is not correct"); | |
System.out.println( "chunk.length = "+chunk.length()); | |
System.out.println( "ChunkSize = "+chunkSize); | |
System.out.println( "chunkString "+chunkSizeString); | |
// then we might be on a unix system and lost the CR of a CRLF combo | |
if (System.getProperty("os.name").toLowerCase( ).startsWith( | |
"windows")) | |
{ | |
System.out.println( "Warning: Chunksize is not correct. On windows so not attempting to fix"); | |
} | |
else | |
{ | |
System.out.println( "Warning: Chunksize is not correct. Attempting to correct with additional \\r\\n"); | |
String newChunk = chunk.replaceAll(""+LF, CRLF); | |
if(newChunk.length()+chunkSizeString.trim().length()+2 !=chunkSize) | |
{ | |
System.err.println( "Attempt to correct the chunksize failed"); | |
} | |
} | |
} | |
theCorrectedResponse.append(chunkSizeString); | |
// append the chunk to the rest of the correct messages | |
theCorrectedResponse.append(chunk); | |
} | |
// append the last chunk on | |
// sometimes the chunk already has a \n on it so we need to remove it | |
if(theCorrectedResponse.charAt(theCorrectedResponse.length()-1) == LF) | |
{ | |
theCorrectedResponse.deleteCharAt(theCorrectedResponse.length()-1); | |
} | |
theCorrectedResponse.append(CRLF+"0"+DOUBLE_CRLF); | |
return theCorrectedResponse.toString(); | |
} | |
/** | |
* @param sResponse | |
* @param iIndex | |
* @param iEOL | |
*/ | |
private static String getResponseLine(String sResponse, int iIndex, int iEOL) | |
{ | |
String sLine=sResponse.substring(iIndex, iEOL); | |
if ((!sLine.endsWith(CRLF))&&(iEOL>iIndex)) | |
{ | |
sLine=sResponse.substring(iIndex, iEOL-1)+CRLF; | |
} | |
return sLine; | |
} | |
private String correctHTTPHeaderSection(String response) | |
{ | |
String returnedResponse =""; | |
// Process HTTP header block. At the end of each line in | |
// the header block, if there is only a LF character, | |
// prefix it with a CR. The end of the HTTP header block | |
// is denoted by an empty line (i.e. CR LF). | |
String sLine; | |
int iIndex=0; | |
int iEoL=response.indexOf(LF, iIndex)+1; | |
do | |
{ | |
sLine=getResponseLine(response, iIndex, iEoL); | |
returnedResponse+=sLine; | |
// Get next line. | |
iIndex=iEoL; | |
iEoL=response.indexOf(LF, iIndex)+1; | |
} | |
while (iEoL>0&&!sLine.equals(CRLF)); | |
if (chunked) | |
return returnedResponse; | |
else | |
{ | |
// Just in case HTTP header has ###, indication that | |
// the Content-Length value needs to be set to the size | |
// of the body of the http response | |
String returnedHttpBody = response.substring(iIndex); | |
returnedResponse = returnedResponse.replaceFirst("###", Integer.toString(returnedHttpBody.length())); | |
return returnedResponse + returnedHttpBody; | |
} | |
} | |
} |