/* * Copyright (C) 2000 - 2008 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/ */ package com.naryx.tagfusion.cfm.engine; import java.util.List; import com.nary.util.string; import com.naryx.tagfusion.cfm.tag.cfDUMP; /** * This class represents the underlying data structure for all dynamic data * types in the engine. * */ public abstract class cfData extends Object implements java.io.Serializable { static final long serialVersionUID = 1; // added constants below for javacast implementation public static enum JavaCast { INT, LONG, BOOLEAN, DOUBLE, FLOAT, STRING, BIGDECIMAL, BYTE, CHAR, SHORT, INT_ARRAY, LONG_ARRAY, BOOLEAN_ARRAY, DOUBLE_ARRAY, FLOAT_ARRAY, STRING_ARRAY, BIGDECIMAL_ARRAY, BYTE_ARRAY, CHAR_ARRAY, SHORT_ARRAY }; public final static byte UNKNOWN = 0, CFNUMBERDATA = 1, CFBOOLEANDATA = 2, CFSTRINGDATA = 3, CFDATEDATA = 4, CFNULLDATA = 5, CFSTRUCTDATA = 10, CFARRAYDATA = 11, CFQUERYRESULTDATA = 15, CFCOMPONENTOBJECTDATA = 20, CFWSOBJECTDATA = 21, CFJAVAOBJECTDATA = 25, CFBINARYDATA = 28, CFLDATA = 30, CFUDFDATA = 40, OTHER = 127; /*** * ATTENTION! Subclasses of cfData that create static instances MUST override * the setQueryTableData() method and throw an exception if that method is * invoked. It's probably a good idea to override the setJavaCast(), * setReference(), setImplicit(), and setExpression() methods and also throw * exceptions if these are invoked. * * Basically, none of the private cfData attributes should be allowed to be * changed from their defaults for static subclass instances. */ private Javacast javacast; private List<List<cfData>> queryTableData; // for regular queries private int queryColumn; private boolean expression; // if true, the instance may contain an expression // that needs to be evaluated private boolean isImplicit; private boolean invalidLoopIndex = false; // valid unless explicitly // invalidated (default to false for // compatible deserialization of 6.2.1 client data) /** * invalidLoopIndex is used to enhance performance of CFLOOP. The idea is that * a cfData subclass being used as a CFLOOP index can be modified directly * rather than creating a new cfData instance. For example, when looping over * a numeric range, rather than creating a new cfNumberData every time the * index is incremented, just update the same cfNumberData instance. * * This doesn't work is when the loop index is being stored in a shared scope * that uses "native" J2EE format ((request, session, or application). For * example: * * <cfloop from="1" to="10" index="request.loopIndex"> ... </cfloop> * * In this case, modifying the cfData instance doesn't modify the shared scope * variable. Therefore, the loop index needs to be "invalidated" to let the * CFLOOP code know that it can't simply modify the cfData instance. * * Also, if the loop index variable is assigned a new value within the loop * body, then the loopIndex needs to be invalidated; for example: * * <cfloop from="1" to="10" index="i"> <cfset i = i + 1> </cfloop> * * See the CFLOOP code that references the isValidLoopIndex() method. */ public void invalidateLoopIndex() { invalidLoopIndex = true; } public boolean isValidLoopIndex() { return !invalidLoopIndex; } public boolean isExpression() { return expression; } public boolean isLoopIndex(){ return false; } public void setExpression(boolean _exp) { expression = _exp; } // --[ returns the type of cfData. Use instead of instanceof when you already // know you have a cfData object. public byte getDataType() { return UNKNOWN; } public String getDataTypeName() { return "unknown"; } // allows subclasses of cfStructData to identify themselves as such when they // need to use a different data type (such as CFCOMPONENTOBJECTDATA) public boolean isStruct() { return false; } public int getQueryColumn() { return queryColumn; } public List<List<cfData>> getQueryTableData() { return queryTableData; } public void setQueryTableData(List<List<cfData>> _queryTableData, int _queryColumn) { queryTableData = _queryTableData; queryColumn = _queryColumn; } public cfData getData(String _key) { return null; } public cfData getData(cfData arrayIndex) throws cfmRunTimeException { return (queryTableData != null) ? cfQueryResultData.getCellData(queryTableData, arrayIndex.getInt(), queryColumn) : null; } public void setData(String _key, cfData _data) { } public void setData(cfData arrayIndex, cfData _data) throws cfmRunTimeException { if (queryTableData != null) cfQueryResultData.setCellData(queryTableData, arrayIndex.getInt(), queryColumn, _data); } public void deleteData(String _key) throws cfmRunTimeException { } /** * This method and it's use was added to fix bug #2083. Any cfdata that's * created which we wish to ensure is not seen by or accessible to a person's * cfml code should set the isImplicit flag to true. * * @return true if this object is considered to be for BlueDragon use only and * should not be accessible to a person's cfml code, else false. */ public boolean isImplicit() { return isImplicit; } protected void setImplicit(boolean implicit) { isImplicit = implicit; } public cfData duplicate() { return null; } public cfData coerce(byte toDataType) throws dataNotSupportedException { if (this.getDataType() == toDataType) // no need to convert return this; switch (toDataType) { case CFNUMBERDATA: return this.getNumber(); case CFBOOLEANDATA: return cfBooleanData.getcfBooleanData(this.getBoolean(), this.getString()); case CFSTRINGDATA: return new cfStringData(this.getString()); case CFDATEDATA: return this.getDateData(); default: throw new dataNotSupportedException(); } } /* * returns true if the cfData passed in is a String, Number, Boolean or Date; * false otherwise. Note that cfLData's will not be evaluated by this function * hence will return false in this case. */ public static boolean isSimpleValue(cfData _d) { return isSimpleValue(_d.getDataType()); } private static boolean isSimpleValue(byte dataType) { // UNKNOWN is used by CFUndefinedValue, and can be converted to a string return ((dataType >= UNKNOWN) && (dataType <= CFNULLDATA)); } // check for number without throwing exception public boolean isNumberConvertible() { return false; } public double getDouble() throws dataNotSupportedException { throw new dataNotSupportedException(); } public String getString() throws dataNotSupportedException { throw new dataNotSupportedException(); } // check for boolean without throwing exception public boolean isBooleanConvertible() { return false; } public boolean getBoolean() throws dataNotSupportedException { throw new dataNotSupportedException(); } // check for date without throwing exception public boolean isDateConvertible() { return false; } public cfDateData getDateData() throws dataNotSupportedException { throw new dataNotSupportedException(); } public long getLong() throws dataNotSupportedException { throw new dataNotSupportedException(); } public long getDateLong() throws dataNotSupportedException { throw new dataNotSupportedException(); } public int getInt() throws dataNotSupportedException { throw new dataNotSupportedException(); } public cfNumberData getNumber() throws dataNotSupportedException { throw new dataNotSupportedException(); } public String toString() { return ""; } public String toString(String _label) { return toString(); } public String getName() { return ""; } public Javacast getJavaCast() { return javacast; } public void setJavaCast( Javacast _cast ) { javacast = _cast; } public boolean equals(cfData _data) throws cfmRunTimeException { throw new cfmRunTimeException(catchDataFactory.generalException("errorCode.expressionError", "cfdata.Equals")); } // the default behavior of CFDUMP is to do a "short" dump; if your tag only // supports one dump style, then only override this method (not dumpLong, // below) public void dump(java.io.PrintWriter out) { dump(out, "", cfDUMP.TOP_DEFAULT); } // if a cfData supports the LABEL attribute it must override this method public void dump(java.io.PrintWriter out, String _label, int _top) { out.print(this.toString(_label)); } // by default, a "long" dump is the same as a short dump; if your tag supports // both long and short dumps then override this method and the one above public void dumpLong(java.io.PrintWriter out) { dump(out); } public void dumpLong(java.io.PrintWriter out, String _label, int _top) { dump(out, _label, _top); } public void dumpWDDX(int version, java.io.PrintWriter out) { } /** * returns a cfNumberData from the given String '_s'. If however the String * represents a long or float and _strict is false then it returns a * cfStringData WARNING - do not use unless you know that _s is definitely a * numeric */ public static cfData createNumber(String _str, boolean _strict) { String numStr = _str; if (numStr.charAt(0) == '+') { numStr = numStr.substring(1); } // if an int/long if (numStr.indexOf(".") == -1) { if (string.isInt(numStr)) { return new cfNumberData(Integer.parseInt(numStr), _str); } else if (_strict) { try { return new cfNumberData(Double.parseDouble(numStr), _str); } catch (java.lang.ArithmeticException ae) { return new cfNumberData(Float.parseFloat(numStr), _str); } } else { return new cfStringData(numStr); } // else if a double/float } else { double dblval; try { dblval = Double.parseDouble(numStr); } catch (java.lang.ArithmeticException ae) { dblval = Float.parseFloat(numStr); if (!_strict) { return new cfStringData(numStr); } else { return new cfNumberData(dblval, _str); } } if (!_strict && (dblval > Double.MAX_VALUE || dblval < Double.MIN_VALUE)) { return new cfStringData(numStr); } else { return new cfNumberData(dblval); } } } /** * This behaves as createNumber() but will however perform a check first to * ensure the String represents a valid numeric. * * @throws dataNotSupportedException * if _s is not a valid numeric */ public static cfData createNumber_Validate(String _s, boolean _strict) throws dataNotSupportedException { String str = _s.trim(); if (string.isNumber(str)) { return createNumber(str, _strict); } else { throw new dataNotSupportedException("value [" + _s + "] is not a number"); } } /** * compares 2 cfData's returns 1 if _cfd1 > _cfd2 returns 0 if _cfd1 == _cfd2 * returns -1 if _cfd1 < _cfd2 * * NOTE : this is used in QOQ for ORDER BY and MAX/MIN. Changes in this method * for other use should be tested against QOQ */ public static int compare(cfData _cfd1, cfData _cfd2) { int dType1 = _cfd1.getDataType(); int dType2 = _cfd2.getDataType(); int compResult = 0; // if both numbers then do a numeric comparison if (dType1 == cfData.CFNUMBERDATA && dType2 == cfData.CFNUMBERDATA) { double double1 = ((cfNumberData) _cfd1).getDouble(); double double2 = ((cfNumberData) _cfd2).getDouble(); if (double1 > double2) { compResult = 1; } else if (double1 < double2) { compResult = -1; } else { compResult = 0; } // else if one of them numbers, try first to do a number comparsion } else if (dType1 == cfData.CFNUMBERDATA || dType2 == cfData.CFNUMBERDATA) { if (dType1 == cfData.CFNULLDATA) { return -1; } else if (dType2 == cfData.CFNULLDATA) { return 1; } try { double double1 = _cfd1.getDouble(); double double2 = _cfd2.getDouble(); if (double1 > double2) { compResult = 1; } else if (double1 < double2) { compResult = -1; } else { compResult = 0; } } catch (dataNotSupportedException dse) { try { compResult = _cfd1.getString().compareTo(_cfd2.getString()); } catch (dataNotSupportedException dse2) { return 0; } } } else if (dType1 == cfData.CFDATEDATA || dType2 == cfData.CFDATEDATA) { try { long lDate1 = _cfd1.getDateData().getLong(); long lDate2 = _cfd2.getDateData().getLong(); if (lDate1 > lDate2) { compResult = 1; } else if (lDate1 < lDate2) { compResult = -1; } else { compResult = 0; } } catch (dataNotSupportedException dse2) { try { compResult = _cfd1.getString().compareTo(_cfd2.getString()); } catch (dataNotSupportedException e) { return 0; } } // else if we can convert both to numbers, try to do a number comparsion } else if (_cfd1.isNumberConvertible() && _cfd1.isNumberConvertible()) { try { double double1 = _cfd1.getDouble(); double double2 = _cfd2.getDouble(); if (double1 > double2) { compResult = 1; } else if (double1 < double2) { compResult = -1; } else { compResult = 0; } } catch (dataNotSupportedException dse) { try { compResult = _cfd1.getString().compareTo(_cfd2.getString()); } catch (dataNotSupportedException dse2) { return 0; } } } else { try { compResult = _cfd1.getString().compareTo(_cfd2.getString()); } catch (dataNotSupportedException dse2) { return 0; } } return compResult; }// compare() /** * This method designed for use by CFSWITCH/CFCASE. It "normalizes" a cfData * instance to a string as follows: * * - If the cfData instance is a cfBooleanData, then return "1" or "0". - If * the cfData instance is a cfNumberData, then return the value of * this.getString() - If the cfData instance is a string that can be converted * to a number, then create a cfNumberData instance and return the * cfNumberData.getString() value. - If the cfData instance is a string with * the value of "true/yes" then return "1", as it would for a boolean. - If * the cfData instance is a string with the value of "false/no" then return * "0", as it would for a boolean. - In all other cases, return the value of * this.getString(). * * Most importantly, it does all this without throwing any exceptions. */ public String toNormalString() throws dataNotSupportedException { if (this.getDataType() == CFBOOLEANDATA) { return (this.getBoolean() ? "1" : "0"); } String dataString = this.getString().toLowerCase(); if (this.getDataType() != CFNUMBERDATA) { dataString = toNormalString(dataString); } return dataString; } /** * dataString is expected to be converted to lowercase before invoking this * method */ public static String toNormalString(String dataString) throws dataNotSupportedException { if (string.isNumber(dataString)) { dataString = cfData.createNumber(dataString, true).getString(); } else if (dataString.equals("true") || dataString.equals("yes")) { dataString = "1"; } else if (dataString.equals("false") || dataString.equals("no")) { dataString = "0"; } return dataString; } }