/* * Copyright (C) 2000 - 2011 TagServlet Ltd * * This file is part of Open BlueDragon (OpenBD) CFML Server Engine. * * OpenBD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * Free Software Foundation,version 3. * * OpenBD 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 OpenBD. If not, see http://www.gnu.org/licenses/ * * Additional permission under GNU GPL version 3 section 7 * * If you modify this Program, or any covered work, by linking or combining * it with any of the JARS listed in the README.txt (or a modified version of * (that library), containing parts covered by the terms of that JAR, the * licensors of this Program grant you additional permission to convey the * resulting work. * README.txt @ http://www.openbluedragon.org/license/README.txt * * http://www.openbluedragon.org/ * * $Id: $ */ package com.naryx.tagfusion.cfm.engine; import java.io.ByteArrayOutputStream; import java.io.CharArrayWriter; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.List; import com.nary.util.string; import com.nary.util.date.dateTimeTokenizer; /** * This class represents string data */ public class cfStringData extends cfJavaObjectData { static final long serialVersionUID = 1; public static cfStringData EMPTY_STRING = new cfStringDataStatic(""); // WARNING! There should be no references to the "data" attribute except in // the constructors, the getString() method, and the getLength() method; all // other references to "data" should be done via the getString() method // // The concept here is that if we construct a cfStringData instance using a // byte[], we're going to defer converting that byte[] to a string until // someone actually needs it by invoking getString(); this saves an enormous // amount of processing time for TEXT, LONGVARCHAR, and CLOB columns // // Note: one of "data" or "dataBytes" will always be null, but never both private String data; private byte[] dataBytes; private char[] dataChars; private transient cfDateData dateData; // the date representation of this string private boolean maybeDateConvertible = true; // assume true until proven otherwise private transient cfNumberData numberData; // the numeric representation of this string private boolean maybeNumberConvertible = true; // assume true until proven otherwise private transient cfBooleanData booleanData; // the boolean representation of this string private boolean maybeBooleanConvertible = true; // assume true until proven otherwise public cfStringData( String _data ){ super( _data ); data = ( _data == null ? "" : _data ); } public cfStringData( byte[] _dataBytes ) { super( null ); dataBytes = _dataBytes; if ( _dataBytes == null ) data = ""; } public cfStringData( char[] _dataChars ) { super( null ); dataChars = _dataChars; if ( _dataChars == null ) data = ""; } public void setString( String _data ) { super.setInstance( _data ); data = ( _data == null ? "" : _data ); dataBytes = null; dataChars = null; maybeDateConvertible = true; // assume true until proven otherwise dateData = null; maybeNumberConvertible = true; // assume true until proven otherwise numberData = null; maybeBooleanConvertible = true; // assume true until proven otherwise booleanData = null; } public static cfStringData getString( InputStream in ) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte temp[] = new byte[ 2048 ]; int bytesRead; while ( ( bytesRead = in.read( temp ) ) != -1 ) out.write( temp, 0, bytesRead ); return new cfStringData( out.toByteArray() ); } public static cfStringData getString( Reader in ) throws IOException { CharArrayWriter out = new CharArrayWriter(); char temp[] = new char[ 2048 ]; int charsRead; while ( ( charsRead = in.read( temp ) ) != -1 ) out.write( temp, 0, charsRead ); return new cfStringData( out.toCharArray() ); } public byte getDataType(){ return cfData.CFSTRINGDATA; } public String getDataTypeName() { return "string"; } public String getString() { if ( data == null ) { // if data is null, one of dataBytes or dataChars will always be non-null if ( dataChars != null ) { setString( new String( dataChars ) ); } else { setString( new String( dataBytes ) ); } } return data; } public int getLength() { // data, dataBytes, and dataChars will never all be null if ( data != null ) { return data.length(); } else if ( dataChars != null ) { return dataChars.length; } else { return dataBytes.length; } } /* trimTrailing Trims the trailing whitespace from a string. */ private static String trimTrailing( String s ) { if ( s == null || s.length() == 0 ) return s; int pos = s.length() - 1; while ( pos > 0 ) { char ch = s.charAt( pos ); if ( ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r' ) break; pos--; } if ( pos == s.length() - 1 ) return s; return s.substring( 0, pos+1 ); } public boolean getBoolean() throws dataNotSupportedException { if ( isBooleanConvertible() ) { return booleanData.getBoolean(); } throw new dataNotSupportedException( "value [" + getString() + "] cannot be converted to a boolean" ); } public boolean isBooleanConvertible() { return isBooleanConvertible( true ); } // has the side-effect of setting the booleanData attribute private boolean isBooleanConvertible( boolean convertNumbers ) { if ( maybeBooleanConvertible ) { if ( booleanData != null ) { return true; } // CFMX will trim any trailing whitespace characters before trying to convert to a boolean // so let's do the same. Refer to bug #2298. String data = trimTrailing( getString() ); String lcaseData = data.toLowerCase(); if ( lcaseData.equals( "1" ) || lcaseData.equals( "true" ) || lcaseData.equals( "yes" ) ) { booleanData = cfBooleanData.TRUE; return true; // boolean convertible } else if ( lcaseData.equals( "0" ) || lcaseData.equals( "false" ) || lcaseData.equals( "no" ) ) { booleanData = cfBooleanData.FALSE; return true; // boolean convertible } else if ( convertNumbers && isNumberConvertible( false ) ) { booleanData = cfBooleanData.getcfBooleanData( numberData.getDouble() != 0.0 ); return true; // boolean convertible } maybeBooleanConvertible = false; // not convertible } return false; } public int getInt() throws dataNotSupportedException { return getNumber().getInt(); } public double getDouble() throws dataNotSupportedException { return getNumber().getDouble(); } public long getLong() throws dataNotSupportedException { return getNumber().getLong(); } public cfNumberData getNumber() throws dataNotSupportedException { if ( isNumberConvertible() ) { return numberData; } throw new dataNotSupportedException( "value [" + getString() + "] is not a number" ); } // returns true if this string can be converted to a number public boolean isNumberConvertible() { return isNumberConvertible( true ); } // has the side-effect of setting the numberData attribute private boolean isNumberConvertible( boolean convertBooleans ) { if ( maybeNumberConvertible ) { if ( numberData != null ) { return true; } String str = getString().trim(); if ( string.isNumber( str ) ) { numberData = (cfNumberData)cfData.createNumber( str, true ); return true; } // if can be converted to boolean, then can be converted to number if ( convertBooleans && isBooleanConvertible( false ) ) { numberData = new cfNumberData( booleanData.getBoolean() ? 1 : 0 ); return true; } maybeNumberConvertible = false; // can't be converted } return false; } public cfData duplicate() { return new cfStringData( getString() ); } public String toString(){return getString();} public String getName(){ return "\"" + getString() + "\""; } public cfDateData getDateData() throws dataNotSupportedException { if ( isDateConvertible() ) { return dateData; } throw new dataNotSupportedException( "Invalid date/time string: " + getString() ); } public long getDateLong() throws dataNotSupportedException { if ( isDateConvertible() ) { return dateData.getLong(); } return ( getLong() * 86400000 ); // throws dataNotSupportedException if not number convertible } // has the side-effect of setting the dateData attribute public boolean isDateConvertible() { return isDateConvertible( true ); } public boolean isDateConvertible( boolean convertNumbers ) { if ( maybeDateConvertible ) { if ( dateData != null ) { return true; } String thisString = getString(); if ( thisString.length() > 0 ) { java.util.Date d = dateTimeTokenizer.getUSDate( thisString ); // this could be made more efficient if a more flexible method is added to DTT if ( d == null ) { d = dateTimeTokenizer.getUKDate( thisString ); } if ( d == null ) { d = dateTimeTokenizer.getNeutralDate( thisString ); } if ( d != null ) { dateData = new cfDateData( d.getTime() ); return true; } // if can be converted to number, then can be converted to date if ( convertNumbers && isNumberConvertible( false ) ) { int asInt = numberData.getInt(); if ( asInt > -693595 && asInt < 2958464) { dateData = numberData.getDateData(); return true; } } } maybeDateConvertible = false; // can't be converted } return false; } // this version of equals() is for use by the CFML expression engine public boolean equals( cfData _data ) throws cfmRunTimeException{ if ( _data.getDataType() == cfData.CFSTRINGDATA ){ return ( (cfStringData) _data ).getString().equals( getString() ); }else if ( _data.getDataType() == cfData.CFNUMBERDATA ){ return ( (cfNumberData) _data ).getString().equals( getString() ); }else{ return super.equals( _data ); // throw unsupported exception } } // this version of equals() is for use by generic Colletions classes public boolean equals( Object o ) { if ( o instanceof cfStringData ){ return ((cfStringData)o).getString().equals( getString() ); }else if ( o instanceof cfNumberData ){ return ( (cfNumberData) o ).getString().equals( getString() ); } return false; } public int hashCode() { return ( getString().hashCode() ); } public void dump( java.io.PrintWriter out, String _label, int _top ){ dump( out ); } public void dump( java.io.PrintWriter out ) { String thisString = getString(); out.print( thisString.length() > 0 ? com.nary.util.string.escapeHtml( thisString ) : "[empty string]" ); } public void dumpWDDX( int version, java.io.PrintWriter out ){ if ( version > 10 ) out.write( "<s>" ); else out.write( "<string>" ); char [] chars = getString().toCharArray(); for ( int i = 0; i < chars.length; i++ ){ switch ( chars[i] ){ case '<': out.write( "<" ); break; case '>': out.write( ">" ); break; case '&': out.write( "&" ); break; default: if ( chars[i] <= 0x1f && chars[i] >= 0 ){ if ( version > 10 ) out.write( "<c c='" ); else out.write( "<char code='" ); String hexStr = Integer.toHexString( chars[i] ); out.write( hexStr.length() == 1 ? "0" + hexStr : hexStr ); out.write( "'/>" ); }else{ out.write( chars[i] ); } } } if ( version > 10 ) out.write( "</s>" ); else out.write( "</string>" ); } /** * private subclass for static instances */ private static class cfStringDataStatic extends cfStringData { private static final long serialVersionUID = 1L; public cfStringDataStatic( String s ) { super( s ); } /** * The following methods are not allowed to be invoked for static instances. */ public void setQueryTableData( List<List<cfData>> queryTableData, int queryColumn ) { throw new UnsupportedOperationException( "static instance" ); } public void setExpression( boolean exp ) { throw new UnsupportedOperationException( "static instance" ); } protected void setImplicit( boolean implicit ) { throw new UnsupportedOperationException( "static instance" ); } } synchronized protected void createInstance() throws cfmRunTimeException { // another thread may have executed this method whilst this thread waited queued on this method if ( instance != null ) return; instance = data; setCfmlPageContext( this ); } public Class<String> getInstanceClass() { return String.class; } }