/*
* Copyright (C) 2000-2015 aw2.0 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.openbd.org/
* $Id: userDefinedFunction.java 2496 2015-02-01 02:19:29Z alan $
*/
package com.naryx.tagfusion.cfm.parser.script;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.nary.util.string;
import com.naryx.tagfusion.cfm.engine.catchDataFactory;
import com.naryx.tagfusion.cfm.engine.cfArgStructData;
import com.naryx.tagfusion.cfm.engine.cfArrayData;
import com.naryx.tagfusion.cfm.engine.cfBooleanData;
import com.naryx.tagfusion.cfm.engine.cfCatchData;
import com.naryx.tagfusion.cfm.engine.cfComponentData;
import com.naryx.tagfusion.cfm.engine.cfData;
import com.naryx.tagfusion.cfm.engine.cfDataFactory;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfJavaObjectData;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfStringData;
import com.naryx.tagfusion.cfm.engine.cfStructData;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.dataNotSupportedException;
import com.naryx.tagfusion.cfm.file.cfFile;
import com.naryx.tagfusion.cfm.parser.CFCall;
import com.naryx.tagfusion.cfm.parser.CFContext;
import com.naryx.tagfusion.cfm.parser.CFUndefinedValue;
import com.naryx.tagfusion.cfm.parser.cfLData;
import com.naryx.tagfusion.cfm.parser.indirectReferenceData;
import com.naryx.tagfusion.cfm.tag.cfFUNCTION;
import com.naryx.tagfusion.cfm.tag.cfFUNCTION.ReturnFormat;
import com.naryx.tagfusion.cfm.tag.cfTag;
import com.naryx.tagfusion.cfm.tag.tagUtils;
import com.naryx.tagfusion.expression.function.functionBase;
public class userDefinedFunction extends functionBase implements java.io.Serializable {
private static final long serialVersionUID = 1;
public static final int ACCESS_PRIVATE = 0;
public static final int ACCESS_PACKAGE = 1;
public static final int ACCESS_PUBLIC = 2;
public static final int ACCESS_REMOTE = 3;
protected String name;
protected cfStructData metaData;
protected int access = -1;
private String returnType;
private Map<String, String> attributes; // the function attributes
// the following attribute is only used for CFFUNCTION-based UDFs
protected cfFUNCTION parentFunction;
// the following two attributes are only used for CFSCRIPT-based UDFs
private CFScriptStatement body;
protected List<CFFunctionParameter> formals; // a list of argument names in Strings
// for UDFs within a CFC; needs to be per-CFC instance
protected cfComponentData superScope;
// for cfScript:Java
protected JavaBlock javaBlock;
protected Method javaMethod;
// for creating CFSCRIPT-based UDFs
public userDefinedFunction(String _name, byte _access, String _returnType, List<CFFunctionParameter> _formals, Map<String, String> _attr, CFScriptStatement _body) {
name = _name;
access = _access;
formals = _formals;
attributes = _attr;
body = _body;
returnType = _returnType;
}
// for creating CFFUNCTION-based UDFs
public userDefinedFunction(String _name, cfFUNCTION _parent) {
name = _name;
parentFunction = _parent;
}
public userDefinedFunction(userDefinedFunction udf, cfComponentData _superScope) {
name = udf.name;
metaData = udf.metaData;
access = udf.access;
attributes = udf.attributes;
returnType = udf.returnType;
parentFunction = udf.parentFunction;
body = udf.body;
formals = udf.formals;
javaBlock = udf.javaBlock;
javaMethod = udf.javaMethod;
superScope = _superScope;
}
public userDefinedFunction(Method method, JavaBlock javaBlock) {
name = method.getName();
this.javaBlock = javaBlock;
this.javaMethod = method;
access = ACCESS_PUBLIC;
formals = new ArrayList<CFFunctionParameter>(2);
}
/*
* This allows for the JavascriptDefinedFunction to properly handle the inheritance stack
*/
public userDefinedFunction duplicateAndInherit(cfComponentData _superScope) {
return new userDefinedFunction(this, _superScope);
}
public cfData duplicate() {
// once constructed, the UDF attributes never change, so no need to create a
// duplicate, which just would be exactly the same as the original
return this;
}
public byte getDataType() {
return cfData.CFUDFDATA;
}
public String getTypeString() {
return "UDF";
}
// for CFFUNCTION-based UDFs only
public List<cfStructData> getFormalArguments() {
if (parentFunction != null) {
return parentFunction.getFormalArguments();
}
return null;
}
public List<CFFunctionParameter> getUDFFormals() {
return formals;
}
public String getName() {
return name;
}
public String getString() {
return name;
}
public cfFUNCTION getParentFunction() {
return parentFunction;
}
public String getParentComponentName() {
String parentComponentName = null;
if (parentFunction != null) {
parentComponentName = parentFunction.getParentComponentName();
}
return (parentComponentName != null ? parentComponentName : "");
}
public boolean isAbstract() {
if (parentFunction != null) {
return parentFunction.isAbstract();
}
return false;
}
public boolean isJavaBlock() {
return (javaBlock != null);
}
public JavaBlock getJavaBlock() {
return javaBlock;
}
public void setJavaBlock(JavaBlock jb) {
javaBlock = jb;
}
public int getAccessType() {
if (access < 0) {
access = ACCESS_PUBLIC; // default access type
cfData accessData = getMetaData().getData(cfFUNCTION.ACCESS);
if (accessData != null) {
String accessString = accessData.toString();
if (accessString.equalsIgnoreCase("PRIVATE")) {
access = ACCESS_PRIVATE;
} else if (accessString.equalsIgnoreCase("PACKAGE")) {
access = ACCESS_PACKAGE;
} else if (accessString.equalsIgnoreCase("REMOTE")) {
access = ACCESS_REMOTE;
}
}
}
return access;
}
public String getAccessTypeString() {
int access = getAccessType();
switch (access) {
case ACCESS_PUBLIC:
return "PUBLIC";
case ACCESS_PRIVATE:
return "PRIVATE";
case ACCESS_PACKAGE:
return "PACKAGE";
case ACCESS_REMOTE:
return "REMOTE";
default:
return "";
}
}
public ReturnFormat getReturnFormat(){
return cfFUNCTION.getReturnFormat( attributes.get("returnformat") );
}
public String getReturnJSONDate(){
if (attributes.containsKey("jsondate")) {
return ((String)attributes.get("jsondate")).toLowerCase();
} else {
return cfEngine.DefaultJSONReturnDate;
}
}
public String getReturnJSONCase(){
if (attributes.containsKey("jsoncase")) {
return ((String)attributes.get("jsoncase")).toLowerCase();
} else {
return cfEngine.DefaultJSONReturnCase;
}
}
public String getReturnType() {
return returnType;
}
// metadata is needed by CFC function calls, also by GetMetaData()
public cfStructData getMetaData() {
if (parentFunction != null) {
return parentFunction.getMetaData();
}
if (metaData == null) {
initMetaData();
}
return metaData;
}
private synchronized void initMetaData() {
metaData = new cfStructData();
metaData.setData("NAME", new cfStringData(name));
metaData.setData(cfFUNCTION.ACCESS, new cfStringData(getAccessTypeString()));
if (this.returnType != null) {
metaData.setData(cfFUNCTION.RETURNTYPE, new cfStringData(returnType));
}
Iterator<String> keyIterator = attributes.keySet().iterator();
while (keyIterator.hasNext()) {
String nextKey = keyIterator.next();
metaData.put(nextKey, attributes.get(nextKey));
}
cfArrayData argArray = cfArrayData.createArray(1);
for (int i = 0; i < formals.size(); i++) {
CFFunctionParameter nextParam = formals.get(i);
cfStructData argStruct = new cfStructData();
argStruct.setData("NAME", new cfStringData(nextParam.getName().toUpperCase()));
argStruct.setData("REQUIRED", new cfStringData(nextParam.isRequired() ? "true" : "false"));
if (nextParam.isDefaulted())
argStruct.setData("DEFAULT", new cfStringData(nextParam.getDefaultAsString()));
if (nextParam.isFormallyTyped())
argStruct.setData("TYPE", new cfStringData(nextParam.getType()));
try {
argArray.addElement(argStruct);
} catch (cfmRunTimeException ignore) {
// will never happen
}
}
metaData.setData("PARAMETERS", argArray);
}
public cfData execute(cfSession _session, List<cfData> _actuals) throws cfmRunTimeException {
return execute(_session, _actuals, true);
}
public cfData execute(cfSession _session, List<cfData> _actuals, boolean _isLocalExec) throws cfmRunTimeException {
if (parentFunction != null) {
if (parentFunction.isAbstract()) {
throw new cfmRunTimeException(catchDataFactory.generalException(_session, cfCatchData.TYPE_TEMPLATE, "Abstract Function Invocation", "Abstract functions may not be invoked"));
}
return parentFunction.run(_session, name, _actuals, superScope, _isLocalExec);
}
// Create the arguments array - this is empty if there are fewer actuals than formals
int nargs = _actuals.size();
cfArgStructData args = new cfArgStructData();
// Instantiate the formals
for (int i = 0; i < formals.size(); i++) {
if (i < nargs) {
// note index increment is down to cfArrayData api that indexes from 1 as opposed to 0.
args.setData(formals.get(i).getName(), _actuals.get(i));
}
}
// Put the remainder into "arguments"
for (int i = formals.size(); i < nargs; i = i + 1) {
// note index increment is down to cfArrayData api that indexes from 1 as opposed to 0.
args.setData(String.valueOf(i + 1), _actuals.get(i));
}
// Invoke the javablock if this function is representing this
if (javaBlock != null) {
org.alanwilliamson.lang.java.inline.ContextImpl.putSession(_session);
if (_actuals.size() == 0) {
try {
if (javaMethod.getReturnType() == null) {
javaMethod.invoke(javaBlock, (Object[]) null);
return cfBooleanData.TRUE;
} else {
Object returnObj = javaMethod.invoke(javaBlock, (Object[]) null);
return tagUtils.convertToCfData(returnObj);
}
} catch (IllegalArgumentException e) {
throwException(_session, "IllegalArgumentException: " + e.getMessage());
return null;
} catch (IllegalAccessException e) {
throwException(_session, "IllegalAccessException: " + e.getMessage());
return null;
} catch (InvocationTargetException e) {
throwException(_session, "InvocationTargetException: " + e.getMessage());
return null;
}
} else {
try {
cfJavaObjectData javaobject = new cfJavaObjectData(javaBlock);
return javaobject.invokeMethod(name, _actuals, _session);
} catch (IllegalArgumentException e) {
throwException(_session, "IllegalArgumentException: " + e.getMessage());
return null;
}
}
} else {
return execute(_session, args, _isLocalExec);
}
}
public cfData execute(cfSession _session, cfArgStructData _args, boolean _isLocalExec) throws cfmRunTimeException {
if (parentFunction != null) { // CFFUNCTION-based UDF call
return parentFunction.run(_session, name, _args, superScope, _isLocalExec);
}
cfComponentData activeComponent = _session.getActiveComponentData();
boolean pushFile = false;
if ( activeComponent != null ){
cfFile thisFile = activeComponent.getComponentFile();
if (thisFile != null) {
pushFile = !thisFile.equals(_session.activeFile());
if (pushFile)
_session.pushActiveFile(thisFile);
}
}
cfTag thisTag = body.getHostTag();
boolean pushTag = false;
if (thisTag != null) {
if (!thisTag.equals(_session.activeTag())) {
_session.pushTag(thisTag);
pushTag = true;
}
}
_session.pushUserDefinedFunction(this);
cfData retVal = null;
CFContext context = _session.getCFContext();
if (context == null) {
throwException(_session, "Internal error : failed to get context");
}
// Create a call environment
CFCall call = _session.enterUDF(_args, superScope, _isLocalExec);
// loop thru the formal args inserting their values to the call scope
for (int i = 0; i < formals.size(); i++) {
CFFunctionParameter nextParam = formals.get(i);
String nextKey = nextParam.getName();
cfData nextData;
if (_args.isNamedBased())
nextData = _args.getData(nextKey);
else
nextData = _args.getData(i);
boolean isDefined = (nextData != null && nextData != CFUndefinedValue.UNDEFINED);
if (!isDefined && nextParam.isDefaulted()) {
nextData = nextParam.getDefaultValue(context);
_args.setData(nextKey, nextData);
} else if (!isDefined && nextParam.isRequired()) {
throwException(_session, "Value not provided for required argument " + nextKey.toUpperCase() + " of function " + name);
} else if (nextData != null && nextParam.isFormallyTyped()) {
// validate type
try {
nextData = cfFUNCTION.coerceArgumentType(_session, nextParam.getType(), nextKey, nextData);
} catch (dataNotSupportedException e) {
throw new cfmRunTimeException(catchDataFactory.generalException(_session, cfCatchData.TYPE_TEMPLATE, "The argument " + nextKey.toUpperCase() + " passed to function is not of type " + nextParam.getType(), "If the parameter type is a component name, it is possible the component could not be found"));
}
}
if (nextData == null || nextData == CFUndefinedValue.UNDEFINED) {
continue;
}
nextData = new indirectReferenceData(nextKey, _args, new cfStringData(nextKey));
call.put(nextKey, nextData, context);
}
// Execute the body.
_session.debugger.startScriptFunction( this );
CFStatementResult result;
boolean output = string.convertToBoolean(this.attributes.get("output"), true);
if (!output) {
cfSession currentSession = context.getSession();
cfSession offlineSession = new cfSession(currentSession, false);
context.setSession(offlineSession);
result = body.Exec(context);
context.setSession(currentSession);
} else {
result = body.Exec(context);
}
if (result == null) {
retVal = CFUndefinedValue.UNDEFINED;
} else if (result.isReturn()) {
retVal = result.getReturnValue();
if ((retVal != null) && (retVal.getDataType() == cfData.CFLDATA)) {
retVal = ((cfLData) retVal).Get(context);
}
// check if trying to return a value when the return type is VOID
if ( "VOID".equalsIgnoreCase( returnType ) && retVal != null && retVal.getDataType() != cfData.CFNULLDATA && !( retVal instanceof CFUndefinedValue ) ){
cfCatchData catchData = new cfCatchData();
catchData.setMessage( "A value cannot be returned when the return type is VOID" );
catchData.setDetail("Function: " + name);
throw new cfmRunTimeException(catchData);
}
try {
retVal = cfFUNCTION.coerceReturnType(context.getSession(), retVal, returnType);
} catch (dataNotSupportedException e) {
// throw error
cfCatchData catchData = new cfCatchData();
catchData.setMessage("The value returned from function " + name + "() is not of type " + returnType);
catchData.setDetail("Function: " + name);
if (returnType.equalsIgnoreCase("UUID") || returnType.equalsIgnoreCase("GUID")) {
catchData.setExtendedInfo("Formal type: " + returnType + ", Return value: " + retVal);
} else {
catchData.setExtendedInfo("Formal type: " + returnType + ", Actual type: " + cfDataFactory.getDatatypeString(retVal));
}
throw new cfmRunTimeException(catchData);
}
} else {
// ### not sure that this should get through here
retVal = cfBooleanData.TRUE;
}
_session.debugger.endScriptFunction( this );
_session.popUserDefinedFunction();
if (pushFile)
_session.popActiveFile();
if (pushTag)
_session.popTag();
_session.leaveUDF();
return retVal;
}
public void dump(PrintWriter out) {
dump(out, "");
}
public void dump(PrintWriter out, String _label) {
if (parentFunction != null) {
parentFunction.dump(out, _label);
return;
}
out.write("<table class='cfdump_table_udf' width=\"100%\">");
out.write("<th class='cfdump_th_udf'>");
if (_label.length() > 0)
out.write(_label + " - ");
out.write("function ");
out.write(name);
// out.write( " (" + this.hashCode() + ")" ); // useful for debugging
out.write("</th>");
out.write("<tr><td class='cfdump_td_value'><table cellspacing=2 width=\"100%\">");
if ((formals == null) || (formals.size() == 0)) {
out.write("<tr><td class='cfdump_td_udf'>Arguments:</td>");
out.write("<td class='cfdump_td_value'>none</td></tr>");
} else {
out.write("<tr><td class='cfdump_td_udf' colspan=2>Arguments:</td></tr>");
out.write("<tr><td class='cfdump_td_udf_args' colspan=2>");
out.write("<table class='cfdump_table_udf_args'>");
out.write("<th class='cfdump_th_udf_args'>Name</th>");
out.write("<th class='cfdump_th_udf_args'>Required</th>");
out.write("<th class='cfdump_th_udf_args'>Type</th>");
out.write("<th class='cfdump_th_udf_args'>Default</th>");
for (int i = 0; i < formals.size(); i++) {
CFFunctionParameter nextFormal = formals.get(i);
out.write("<tr><td class='cfdump_td_value'>" + nextFormal.getName().toLowerCase() + "</td>");
out.write("<td class='cfdump_td_value'>");
out.write(nextFormal.isRequired() ? "Required" : "Optional");
out.write("</td>");
out.write("<td class='cfdump_td_value'>");
out.write(nextFormal.isFormallyTyped() ? nextFormal.getType() : "any");
out.write("</td>");
out.write("<td class='cfdump_td_value'>");
out.write(nextFormal.isDefaulted() ? nextFormal.getDefaultAsString() : "");
out.write("</td></tr>");
}
out.write("</table>");
out.write("</td></tr>");
}
out.write("<tr><td class='cfdump_td_udf' width='50%'>Return Type:</td>");
out.write("<td class='cfdump_td_value'>" + (returnType != null ? returnType.toLowerCase() : "") + "</td></tr>");
out.write("<tr><td class='cfdump_td_udf' width='50%'>Roles:</td>");
out.write("</table></td></tr>");
out.write("</table>");
}
public String toString() {
StringWriter w = new StringWriter();
this.dump(new PrintWriter(w));
return w.toString();
}
}