package railo.runtime.writer; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * JSP Writer that Remove WhiteSpace from given content while preserving pre-formatted spaces * in Tags like <CODE> <PRE> and <TEXTAREA> */ public final class CFMLWriterWSPref extends CFMLWriterImpl implements WhiteSpaceWriter { public static final char CHAR_NL = '\n'; public static final char CHAR_RETURN = '\r'; public static final char CHAR_SPACE = ' '; private static final char CHAR_GT = '>'; private static final char CHAR_LT = '<'; private static final char CHAR_SL = '/'; private static final String[] EXCLUDE_TAGS = { "code", "pre", "textarea" }; private static int minTagLen = 64; private static final boolean doChangeWsToSpace = true; // can help fight XSS attacks with characters like Vertical Tab private int[] depths; private int depthSum = 0; private char lastChar = 0; private boolean isFirstChar = true; private StringBuilder sb = new StringBuilder(); static { // TODO: set EXCLUDE_TAGS (and perhaps doChangeWsToSpace) to values from WebConfigImpl for ( String s : EXCLUDE_TAGS ) if ( s.length() < minTagLen ) minTagLen = s.length(); minTagLen++; // add 1 for LessThan symbol } /** * constructor of the class * @param rsp * @param bufferSize * @param autoFlush */ public CFMLWriterWSPref(HttpServletRequest req, HttpServletResponse rsp, int bufferSize, boolean autoFlush, boolean closeConn, boolean showVersion, boolean contentLength, boolean allowCompression) { super(req,rsp, bufferSize, autoFlush,closeConn,showVersion,contentLength,allowCompression); depths = new int[ EXCLUDE_TAGS.length ]; } /** * prints the characters from the buffer and resets it * * TODO: make sure that printBuffer() is called at the end of the stream in case we have some characters there! (flush() ?) */ synchronized void printBuffer() throws IOException { // TODO: is synchronized really needed here? int len = sb.length(); if ( len > 0 ) { char[] chars = new char[ len ]; sb.getChars( 0, len, chars, 0 ); sb.setLength( 0 ); super.write( chars, 0, chars.length ); } } void printBufferEL() { if( sb.length() > 0 ) { try { printBuffer(); } catch (IOException e) {} } } /** * checks if a character is part of an open html tag or close html tag, and if so adds it to the buffer, otherwise returns false. * * @param c * @return true if the char was added to the buffer, false otherwise */ boolean addToBuffer( char c ) throws IOException { int len = sb.length(); if ( len == 0 && c != CHAR_LT ) return false; // buffer must starts with '<' sb.append( c ); // if we reached this point then we will return true if ( ++len >= minTagLen ) { // increment len as it was sampled before we appended c boolean isClosingTag = ( len >= 2 && sb.charAt( 1 ) == CHAR_SL ); String substr; if ( isClosingTag ) substr = sb.substring( 2 ); // we know that the 1st two chars are "</" else substr = sb.substring( 1 ); // we know that the 1st char is "<" for ( int i=0; i<EXCLUDE_TAGS.length; i++ ) { // loop thru list of WS-preserving tags if ( substr.equalsIgnoreCase( EXCLUDE_TAGS[ i ] ) ) { // we have a match if ( isClosingTag ) { depthDec( i ); // decrement the depth at i and calc depthSum printBuffer(); lastChar = 0; // needed to allow WS after buffer was printed } else { depthInc( i ); // increment the depth at i and calc depthSum } } } } return true; } /** * decrement the depth at index and calc the new depthSum * @param index */ private void depthDec( int index ) { if ( --depths[ index ] < 0 ) depths[ index ] = 0; depthCalc(); } /** * increment the depth at index and calc the new depthSum * @param index */ private void depthInc( int index ) { depths[ index ]++; depthCalc(); } /** * calc the new depthSum */ private void depthCalc() { int sum = 0; for ( int d : depths ) sum += d; depthSum = sum; } /** * sends a character to output stream if it is not a consecutive white-space unless we're inside a PRE or TEXTAREA tag. * * @param c * @throws IOException */ @Override public void print( char c ) throws IOException { boolean isWS = Character.isWhitespace( c ); if ( isWS ) { if ( isFirstChar ) // ignore all WS before non-WS content return; if ( c == CHAR_RETURN ) // ignore Carriage-Return chars return; if ( sb.length() > 0 ) { printBuffer(); // buffer should never contain WS so flush it lastChar = c == CHAR_NL ? CHAR_NL : ( doChangeWsToSpace ? CHAR_SPACE : c ); super.print( lastChar ); return; } } isFirstChar = false; if ( c == CHAR_GT && sb.length() > 0 ) printBuffer(); // buffer should never contain ">" so flush it if ( isWS || !addToBuffer( c ) ) { if ( depthSum == 0 ) { // we're not in a WS-preserving tag; suppress whitespace if ( isWS ) { // this char is WS if ( lastChar == CHAR_NL ) // lastChar was NL; discard this WS char return; if ( c != CHAR_NL ) { // this WS char is not NL if ( Character.isWhitespace( lastChar ) ) return; // lastChar was WS but Not NL; discard this WS char if ( doChangeWsToSpace ) c = CHAR_SPACE; // this char is WS and not NL; change it to a regular space } } } lastChar = c; // remember c as lastChar and write it to output stream super.print( c ); } } /** * @see railo.runtime.writer.CFMLWriter#writeRaw(java.lang.String) */ public void writeRaw(String str) throws IOException { printBuffer(); super.write(str); } /** * just a wrapper function for ACF * @throws IOException */ public void initHeaderBuffer() throws IOException{ resetHTMLHead(); } /** * @see railo.runtime.writer.CFMLWriterImpl#clear() */ public final void clear() throws IOException { printBuffer(); super.clear(); } /** * @see railo.runtime.writer.CFMLWriterImpl#clearBuffer() */ public final void clearBuffer() { printBufferEL(); super.clearBuffer(); } /** * @see railo.runtime.writer.CFMLWriterImpl#close() */ public final void close() throws IOException { printBuffer(); super.close(); } /** * @see railo.runtime.writer.CFMLWriterImpl#flush() */ public final void flush() throws IOException { printBuffer(); super.flush(); } /** * @see railo.runtime.writer.CFMLWriterImpl#getRemaining() */ public final int getRemaining() { printBufferEL(); return super.getRemaining(); } /** * @see railo.runtime.writer.CFMLWriterImpl#newLine() */ public final void newLine() throws IOException { print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(boolean) */ public final void print(boolean b) throws IOException { printBuffer(); super.print(b); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(char[]) */ public final void print(char[] chars) throws IOException { write(chars,0,chars.length); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(double) */ public final void print(double d) throws IOException { printBuffer(); super.print(d); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(float) */ public final void print(float f) throws IOException { printBuffer(); super.print(f); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(int) */ public final void print(int i) throws IOException { printBuffer(); super.print(i); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(long) */ public final void print(long l) throws IOException { printBuffer(); super.print(l); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(java.lang.Object) */ public final void print(Object obj) throws IOException { print(obj.toString()); } /** * @see railo.runtime.writer.CFMLWriterImpl#print(java.lang.String) */ public final void print(String str) throws IOException { write(str.toCharArray(),0,str.length()); } /** * @see railo.runtime.writer.CFMLWriterImpl#println() */ public final void println() throws IOException { print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(boolean) */ public final void println(boolean b) throws IOException { printBuffer(); super.print(b); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(char) */ public final void println(char c) throws IOException { print(c); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(char[]) */ public final void println(char[] chars) throws IOException { write(chars,0,chars.length); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(double) */ public final void println(double d) throws IOException { printBuffer(); super.print(d); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(float) */ public final void println(float f) throws IOException { printBuffer(); super.print(f); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(int) */ public final void println(int i) throws IOException { printBuffer(); super.print(i); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(long) */ public final void println(long l) throws IOException { printBuffer(); super.print(l); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(java.lang.Object) */ public final void println(Object obj) throws IOException { println(obj.toString()); } /** * @see railo.runtime.writer.CFMLWriterImpl#println(java.lang.String) */ public final void println(String str) throws IOException { print(str); print(CHAR_NL); } /** * @see railo.runtime.writer.CFMLWriterImpl#write(char[], int, int) */ public final void write(char[] chars, int off, int len) throws IOException { for(int i=off;i<len;i++) { print(chars[i]); } } /** * @see railo.runtime.writer.CFMLWriterImpl#write(java.lang.String, int, int) */ public final void write(String str, int off, int len) throws IOException { write(str.toCharArray(),off,len); } /** * @see railo.runtime.writer.CFMLWriterImpl#write(char[]) */ public final void write(char[] chars) throws IOException { write(chars,0,chars.length); } /** * @see railo.runtime.writer.CFMLWriterImpl#write(int) */ public final void write(int i) throws IOException { print(i); } /** * @see railo.runtime.writer.CFMLWriterImpl#write(java.lang.String) */ public final void write(String str) throws IOException { write(str.toCharArray(),0,str.length()); } }