
/*

  SmartClient Ajax RIA system
  Version v9.1p_2021-05-18/LGPL Deployment (2021-05-18)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

if(window.isc&&window.isc.module_Core&&!window.isc.module_DataBinding){isc.module_DataBinding=1;isc._moduleStart=isc._DataBinding_start=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc._moduleEnd&&(!isc.Log||(isc.Log && isc.Log.logIsDebugEnabled('loadTime')))){isc._pTM={ message:'DataBinding load/parse time: ' + (isc._moduleStart-isc._moduleEnd) + 'ms', category:'loadTime'};
if(isc.Log && isc.Log.logDebug)isc.Log.logDebug(isc._pTM.message,'loadTime');
else if(isc._preLog)isc._preLog[isc._preLog.length]=isc._pTM;
else isc._preLog=[isc._pTM]}isc.definingFramework=true;

if (window.isc && isc.version != "v9.1p_2021-05-18/LGPL Deployment") {
    isc.logWarn("SmartClient module version mismatch detected: This application is loading the core module from "
        + "SmartClient version '" + isc.version + "' and additional modules from 'v9.1p_2021-05-18/LGPL Deployment'. Mixing resources from different "
        + "SmartClient packages is not supported and may lead to unpredictable behavior. If you are deploying resources "
        + "from a single package you may need to clear your browser cache, or restart your browser."
        + (isc.Browser.isSGWT ? " SmartGWT developers may also need to clear the gwt-unitCache and run a GWT Compile." : ""));
}




//> @class TextSettings
// Common base class of +link{TextImportSettings()}.
// @treeLocation Client Reference/System
// @visibility external
//<
isc.ClassFactory.defineClass("TextSettings");

isc.TextSettings.addClassProperties({
    //> @type EscapingMode
    // Mode for escaping text values when using +link{DataSource.recordsAsText()} or
    // +link{DataSource.recordsFromText()}.
    DOUBLE: "double",
    // @value "double" Literal double quotes in data values are doubled (""), as expected by Microsoft
    //                 Excel when pasting text values
    BACKSLASH: "backslash"
    // @value "backslash" double quotes in data values have a blackslash (\) prepended, similar to
    //                    String escaping in JavaScript and Java
    // @visibility external
    //<
});

isc.TextSettings.addProperties({

    //> @attr textSettings.fieldList (Array of String : null : IR)
    // For export, a set of fields to export.  Default is to export all DataSource fields.
    // <P>
    // Fields may be specified that are not in the DataSource but for which data values are present
    // in the provided Records.  In this case the field is assumed to be of type "text".
    // <p>
    // For import, names of DataSource fields to use to parse values, in order.
    // <P>
    // If <code>fieldList</code> is unset, DataSource fields are used, in order.
    // <P>
    // If more values exist in a given Record than the listed fields or than all DataSource fields,
    // remaining values are ignored.
    // @visibility external
    //<
    fieldList: null,

    //> @attr textSettings.fieldSeparator (String : "," : IR)
    // Separator between field values.  Default is a comma character, producing CSV
    // (comma-separated values) format.
    // @visibility external
    //<
    fieldSeparator: ",",

    //> @attr textSettings.lineSeparator (String : null : IR)
    // Separator between Records.  For import, default of null means that either the Unix/Mac
    // format of just a newline ("\n") or the typical DOS/Windows format of a carriage return
    // and newline ("\r\n") will be accepted.  For export, overridden in +link{TextExportSettings}.
    // @visibility external
    //<
    lineSeparator: null,

    //> @attr textSettings.escapingMode (EscapingMode : "backslash" : IR)
    // +link{EscapingMode} expected for escaping special characters embedded in text values.
    // @visibility external
    //<
    escapingMode: isc.TextSettings.BACKSLASH
});

isc.TextSettings.addMethods({

    getEscapingModeEscapeChar : function () {
        switch (this.escapingMode) {
        case isc.TextSettings.DOUBLE:
            return "\"";
            break;
        case isc.TextSettings.BACKSLASH:
            return "\\";
            break;
        }
        return "";
    }

});




//> @class TextExportSettings
// Settings for use with +link{DataSource.recordsAsText()}.
// @treeLocation Client Reference/System
// @visibility external
//<
isc.ClassFactory.defineClass("TextExportSettings", "TextSettings");

isc.TextExportSettings.addClassProperties({
    //> @type ForceTextApproach
    // Approach to force a text value to be interpreted as text rather than parsed as a date, time
    // or other structured types, as can happen with Microsoft Excel.  For background information,
    // see +link{group:excelPasting}.
    //
    LEADING_SPACE: "leadingSpace",
    // @value "leadingSpace" a leading space character is added
    FORMULA: "formula"
    // @value "formula" text value is turned into a trivial Excel formula (eg "car" becomes ="car").
    // In Excel, this renders just the value "car" but editing the cell reveals the formula.
    // @visibility external
    //<
});

isc.TextExportSettings.addProperties({
    //> @attr textExportSettings.lineSeparator (String : "\n" : IR)
    // Separator between Records.  Default is a newline character ("\n").
    // @visibility external
    //<
    lineSeparator: "\n",

    //> @attr textExportSettings.quoteValues (Boolean : true : IR)
    // Whether to surround each value with quotes ("").
    // @visibility external
    //<
    quoteValues: true,

    //> @attr textExportSettings.nullValueText (String : "": IR)
    // Text to export for a field with a null value.  If this property is null, then
    // null fields will be assumed to have the default value for their field type.
    // @visibility external
    //<
    nullValueText: "",

    //> @attr textExportSettings.useDisplayValue (Boolean : false : IR)
    // Whether to convert each field's value to the corresponding display value
    // for export.  Default of false will directly export the field's value.
    // @visibility external
    //<
    useDisplayValue: false,

    //> @attr textExportSettings.forceText (ForceTextApproach : null : IR)
    // If set, all text fields will use the indicated +link{ForceTextApproach} unless they have
    // a specific setting for +link{dataSourceField.exportForceText}.
    // @visibility external
    //<
    forceText: null,

    //> @attr textExportSettings.dateFormat (DateDisplayFormat : null : IR)
    // Format to use when outputting date values.  Default is to use the format expected by
    // Microsoft Excel (eg 1-2-2011), which Excel will turn into a real date value (see
    // +link{group:excelPasting}).  The current month-day-year order as set by
    // +link{Date.setInputFormat()} will be used.
    // @visibility external
    //<
    dateFormat: null,

    //> @attr textExportSettings.dateTimeFormat (DateDisplayFormat : null : IR)
    // Format to use when outputting datetime values.  Default is to combine the configured date
    // and time formats with a space (" ").
    // @visibility external
    //<
    dateTimeFormat: null,

    //> @attr textExportSettings.timeFormat (TimeDisplayFormat : null : IR)
    // Format to use when outputting time values.  Default is 24 hour time.
    // @visibility external
    //<
    timeFormat: null
});




//> @class TextImportSettings
// Settings for use with +link{DataSource.recordsFromText()}.
// @treeLocation Client Reference/System
// @visibility external
//<
isc.ClassFactory.defineClass("TextImportSettings", "TextSettings");

isc.TextImportSettings.addClassProperties({

    // This internal property configures the parser to accept JSON object records.
    // The header items are used directly as field names, without checking or
    // matching against DataSource titles or fields, and the records are returned
    // without converting them to objects via +link{DataSource.validateJSONRecord()}.
    _importAsJsonObjects : false
});

isc.TextImportSettings.addProperties({

    //> @attr textImportSettings.hasHeaderLine (Boolean : false : IR)
    // If set to true, the data is assumed to have a header line that lists titles for each field,
    // which should be parsed.
    // <P>
    // <code>recordsFromText</code> will then try to find a same-named
    // DataSourceField by checking parsed titles against both +link{DataSourceField.title} and
    // +link{DataSourceField.name} (titles first), doing a case-insensitive comparison with any
    // leading or trailing whitespace removed from the title.  If no field matches, data will
    // appear in the returned Records under the exact title parsed from the header line.
    // <P>
    // If this approach will not find appropriate DataSourceFields, parse the header line before
    // calling <code>recordsFromText()</code>, and provide the list of field names to use when
    // parsing data as +link{TextSettings.fieldList}.
    // @visibility external
    //<
    hasHeaderLine: false,

    //> @attr textImportSettings.trim (boolean : false : IR)
    // If set to true, calls +link{String.trim} to remove whitespace before and after
    // the value before removing any quotes.
    //<
    trim: false
});

isc.TextImportSettings.addMethods({

    getSpecialCharactersRegExp : function (flags) {
        var expression = this.lineSeparator ? "\\" + this.lineSeparator : "(\r)?\n";
        expression += "|\\" + this.getEscapingModeEscapeChar() + "\"";
        expression += "|\\" + this.fieldSeparator;
        expression += "|\\\"";
        return new RegExp(expression, flags);
    },

    removeUnescapedQuotes : function (value) {
        var escapeChar = this.getEscapingModeEscapeChar();

        var reEscapedQuotes = new RegExp("\\" + escapeChar + "\"", "g");

        var reUnEscapedQuotes = escapeChar == "\"" ?
            new RegExp("([^\"]|^)\"(?!\")", "g") :
            new RegExp("\"\"|([^\\" + escapeChar + "]|^)\"", "g");

        return value.replace(reUnEscapedQuotes, "$1").replace(reEscapedQuotes, "\"");
    },

    addFinalLineSeparatorIfNotPresent : function (text) {
        var lineSeparator = this.lineSeparator || "\n";
        var regExp = new RegExp(lineSeparator + "$");
        if (!text.match(regExp, text)) text += lineSeparator;
        return text;
    },

    parseTextAndApplyFunctions : function (text, fieldFunction, lineFunction) {

        var fieldSeparator = this.fieldSeparator;
        var escapedQuote = this.getEscapingModeEscapeChar() + "\"";
        var boundaryRegExp = this.getSpecialCharactersRegExp();

        var startPos = 0, quoted = false;;

        for (var hit, target = text, offset = 0; null != (hit = target.match(boundaryRegExp));
             target = target.substring(increment), offset += increment) {

            var increment = hit[0].length + hit.index;
            switch(hit[0]) {
            case escapedQuote:
                break;
            case "\"":
                quoted = !quoted;
                break;
            default:
                if (quoted) continue;

                var value = text.substring(startPos, offset + hit.index);
                if (this.trim) value = value.trim();
                // disambiguate single set of double quotes as empty value rather than
                // one escaped quote when TextExportSettings escaping mode is DOUBLE
                if (value == "\"\"" && this.escapingMode == isc.TextSettings.DOUBLE) {
                    value = "";
                }
                value = this.removeUnescapedQuotes(value);
                startPos = offset + increment;

                fieldFunction(value);

                if (hit[0] != fieldSeparator) {
                    if (lineFunction()) target = "";
                }
                break;
            }
        }
        return text.substring(startPos);
    }
});







//
//=    @object    XMLSerialize
//
//    xml serialize() methods for the comm package
//

// XXX this package must not be dependant on the Comm package, because serialization is a useful
// feature completely apart from Comm.  Unfortunately, the methods are currently expected to be on
// the Comm class, so if the Comm class doesn't exist we need to define it.
if (!isc.Comm) isc.defineClass("Comm");

isc.Comm.addClassProperties( {
    // prefixes for special object types

    //>    @classAttr    Comm.XML_BACKREF_PREFIX (String : "$$BACKREF$$:" : IR)
    //        @group    serialization
    //            Prefix for back-references in serialized object references.
    //<
    XML_BACKREF_PREFIX : "$$BACKREF$$:",

    _xmlIdentifierRegex : /^([_:A-Za-z])([_:.A-Za-z0-9]|-)*$/,
    // this property only applies to XMLSerialize
    serializeBackrefs : true
});


isc.Comm.addClassMethods( {

//>    @classMethod    Comm.xmlSerialize()
//            Serialize an object of any type into an xml string, in a form that
//             can be read by server-side code via schemaless transform
//
//        Note: You should call this routine to serialize any type of object,
//                 rather than calling the custom routines on that object...
//
//        @group    xml serialization
//        @param    name        (any)        name of object to be serialized (used for outer XML tag)
//        @param    object        (any)        object to be serialized
//        @param    [indent]    (boolean)    true == output should be indented for reading,
//                                      false == no indentation
//
//        @return                (string)    serialization form of the object
//<
xmlSerialize : function (name, object, indent) {
    return isc.Comm._xmlSerialize(name, object, indent ? "" : null);
},
//>    @classMethod    Comm._xmlSerialize()    (IA)
//            Internal routine that actually does the serialization.
//        @group    serialization
//
//        @param    name    (string)    name of the object for XML tags
//        @param    object    (any)        object to serialize
//        @param    prefix    (string)    string to put before each line of serialization output
//        @param    context (object)    context tracking objects already serialized and path
//                                  traversed so far
//
//        @return    (string)            serialized object as a string
//<
_xmlSerialize : function (name, object, prefix, context) {

    // Avoid attempting to manipulate SGWT Java objects
    if (isc.Browser.isSGWT && window.SmartGWT.isNativeJavaObject(object)){

        if (object == null) object = null;
        // If the global flag has been set to warn when we hit an unconvertible
        // object, do this.
        else {
            if (window.SmartGWT.warnOnSerializeError) {
                window.SmartGWT.throwUnconvertibleObjectException(
                    object, window.SmartGWT.serializeErrorMessage
                );
            }
            object = String.asSource(object + "");
        }
    }

    // record whether a name was explicitly passed
    var namePassed = name != null;

    // NOTE: allow context as a partial object, so eg isRoot can be set
    if (!context || !context.objRefs) {
        context = isc.addProperties({}, context);
        context.objRefs = {obj:[],path:[]};
        if (!context.objPath) {
            if (object && object.getID) context.objPath = object.getID();
            else context.objPath = "";
        }
        if (name == null) {
            if (isc.isA.Class(object)) name = object.getClassName();
            else if (isc.isAn.Array(object)) name = "Array";
            else if (isc.isA.Object(object)) name = object.$schemaId || "Object";
            else name = "ISC_Auto";
        }
    }

    // handle simple types

    // NOTE: in some use cases we need be able to send null, which potentially has a distinct
    // meaning from empty string (""), for example, nulling out a text field vs setting to
    // empty string.  In this case null is encoded distinctly by setting the attribute xsi:nil.
    // Note schema-driven serialization in DataSource.js does a similar thing but only for
    // fields marked nillable:true in schema.
    if (object == null) {
        if (isc.Comm.xmlSchemaMode || !isc.Comm._explicitNils) {
            return isc.Comm._xmlValue(name, "");
        } else {
            // send explicit null
            return isc.Comm._xmlValue(name, null, "nil");
        }
    }

    if (isc.isA.String(object))    {
        return isc.Comm._xmlValue(name, isc.makeXMLSafe(object),
                                  (isc.Comm.xmlSchemaMode ? "string" : null));
    }
    if (isc.isA.Function(object)) {
        if (object.iscAction) return isc.StringMethod._xmlSerializeAction(object.iscAction);
        return null;
    }

    if (object == window) {
        this.logWarn("Serializer encountered the window object at path: " + context.objPath
                    +" - returning null for this slot.");
        return null;
    }

    // XML comm supports strong typing of numbers and booleans, but JS comm does not (the type
    // information is not propagated). Preserving the type is useful, so we default to that - but
    // this can be disabled
    if (isc.RPCManager.preserveTypes) {
        // for numbers, distinguish between float and integer
        // NOTE: special numbers like NaN and Infinity aren't allowed in the XML Schema numeric
        // types - the XML schema approach here would be to declare a union type between a
        // numeric base type and an enum of NaN, Infinity, etc.
        if (isc.isA.Number(object) || isc.isA.SpecialNumber(object)) {
            if (object.toString().contains("."))
                return isc.Comm._xmlValue(name, object, "double");
            return isc.Comm._xmlValue(name, object, "long");
        }
        if (isc.isA.Boolean(object)) return isc.Comm._xmlValue(name, object, "boolean");
    } else {
        // old approach
        if (isc.isA.Number(object) || isNaN(object)) {
            return isc.Comm._xmlValue(name, object);
        }
        if (isc.isA.Boolean(object))    return isc.Comm._xmlValue(name, object);
    }

    // for complex types:

    // detect infinite loops by checking if we've seen this object before.
    // disambiguate between true loops vs the same leaf object being encountered twice
    // (such as a simple Array containing two Strings which appears in two spots).  Only
    // consider this a loop if the preceding occurrence of the object was some parent of
    // ours.
    var prevPath = isc.JSONEncoder._serialize_alreadyReferenced(context.objRefs, object);
    if (prevPath != null && context.objPath.contains(prevPath)) {
        // Note: check that the first char after "prevPath" is a path separator char in order
        // to avoid false loop detection with "prop" and "prop2" having the same non-looping
        // object (since "prop2" contains "prop").
        var nextChar = context.objPath.substring(prevPath.length, prevPath.length+1);
        //this.logWarn("backref: prevPath: " + prevPath + ", current: " + context.objPath +
        //             ", char after prevPath: " + nextChar);
        if (nextChar == "." || nextChar == "[" || nextChar == "]") {
            if (this.serializeBackrefs) {
                return isc.Comm._xmlOpenTag(name) +
                                isc.Comm.XML_BACKREF_PREFIX + prevPath +
                       isc.Comm._xmlCloseTag(name);
            }
            return isc.emptyString;
        }
    }

    // remember Objects and Arrays to avoid infinite loops
    isc.JSONEncoder._serialize_remember(context.objRefs, object, context.objPath);

    // if there is an xmlSerialize method associated with this object, call that
    if (isc.isA.Function(object._xmlSerialize)) {
        return object._xmlSerialize(name, null, null, prefix, context.objRefs, context.objPath);
    } else if (isc.isA.Class(object)) {
        this.logWarn("Attempt to serialize class of type: " + object.getClassName()
                     + " at path: " + context.objPath + " - returning null for this slot.");
        return null;
    }

    // we define the xsi namespace on the first nested object that we encounter.  The first such
    // object sets the value isRoot on the context to 'false' explicitly.  If it's not defined, then
    // it's true.
    var isRoot = context.isRoot == false ? false : true;

    // handle arrays as a special case
    if (isc.isAn.Array(object))
        return isc.Comm._xmlSerializeArray(name, object, context.objPath,
                                           context.objRefs, prefix, isRoot);

    var data;
    // if the object has a getSerializeableFields, use whatever it returns, otherwise just use the object
    if (object.getSerializeableFields) {

        data = object.getSerializeableFields([], []);
    } else {
        data = object;
    }

    return isc.Comm._xmlSerializeObject(name, data, context.objPath,
                                        context.objRefs, prefix, isRoot);
},

//>    @classMethod    Comm._xmlSerializeArray()    (A)
//            Internal routine to serialize an array.
//
//        @group    serialization
//        @param    name    (string)    name of the object for XML tags
//        @param    object    (any)        object to serialize
//        @param    prefix    (string)    string to put before each line of serialization output
//        @param    objRefs    (object[])    array of objects that have been serialized already so
//                                     we don't get into endless loops
//        @param    objPath    (string)    global variable path to this object, for serializing object references
//
//        @return    (string)            serialized object as a string
//<
_xmlSerializeArray : function (name, object, objPath, objRefs, prefix, isRoot) {

    // open xml tag
    var result = isc.Comm._xmlOpenTag(name, "List", null, null, null, isRoot);

    // spin through the array and create <elem>value</elem> strings
    for (var i = 0, len = object.length; i < len; i++) {
        var value = object[i];
        var context = {
            objRefs : objRefs,
            objPath : isc.JSONEncoder._serialize_addToPath(objPath, i),
            isRoot : false
        };
        result = isc.StringBuffer.concat(
                result,
                (prefix != null ? isc.StringBuffer.concat("\r", prefix, "\t") : ""),
                isc.Comm._xmlSerialize((value != null ? value.$schemaId : null) || "elem",
                                       value,
                                       (prefix != null ? isc.StringBuffer.concat(prefix, "\t") : null),
                                       context)
                );
    }

    // close xml tag
    result = isc.StringBuffer.concat(
            result,
            (prefix != null ? isc.StringBuffer.concat("\r", prefix) : ""),
            isc.Comm._xmlCloseTag(name)
            );

    return result;
},

_isValidXMLIdentifier : function (name) {
    // XMLSerialize is used to transform arbitrary JS structures, including object literals
    // with strings for keys.  XML accepts only a subset of characters that are valid in a
    // string.  We encode them in an attribute value and have the server reconstitute them via
    // the special _isc_name encoding.  But requests sent out of band of our server (direct
    // webservices for example) can't be helped in this manner.  For those, we simply punt and
    // expect users to provide valid identifiers.
    //
    // It would be useful to report bad identifiers to the DeveloperConsole.  Unfortunately,
    // the JS regexp character classes aren't powerful enough for us to do this without
    // recapitulating the unicode character ranges verbatim from the spec, which would be slow
    // and take up a lot of space.
    //
    // Note that our regexp matches a subset of the valid identifiers, but this is harmless
    // since our server reconstitues these.
    //
    // Spec is here:
    // http://www.w3.org/TR/REC-xml/#NT-Letter
    return isc.Comm.xmlSchemaMode || name.match(this._xmlIdentifierRegex);
},

//>    @classMethod    Comm._xmlSerializeObject()    (A)
//            Internal routine to serialize an object.
//
//        @group    serialization
//        @param    object    (any)        object to serialize
//        @param    prefix    (string)    string to put before each line of serialization output
//        @param    objRefs    (object[])    array of objects that have been serialized already so
//                                     we don't get into endless loops
//        @param    objPath    (string)    global variable path to this object, for serializing object references
//
//        @return    (string)            serialized object as a string
//<
_xmlSerializeObject : function (name, object, objPath, objRefs, prefix, isRoot) {

    // if it's a class or has the special _constructor property, then the name is the class name
    // this allows us to hand the output of this method to the server-side xml parser and get back
    // a DataSource-validated object back.
    // Aug 2008 - moved this check before the call to isc.Comm._xmlOpenTag, to ensure that it
    // uses the correct name for non-Class objects with a _constructor - without this change,
    // it was returning mismatched open and close tags
    // April 2010 - added "RelativeDate" as a class-name to ignore, so that relative dates
    // can be sent up to the server as part of criteria without having their "value" property
    // renamed
    if (isc.isAn.Instance(object)) name = object.getClassName();
    else if (object._constructor && object._constructor != "AdvancedCriteria" &&
        object._constructor != "RelativeDate") name = object._constructor;

    // open xml tag
    //
    // NOTE: we do need to explicitly label the structure we're about to write out as an "Object",
    // because for a single-property object like { values : { locale : 10 } } we'd currently write:
    // <container>
    //   <values>
    //     <someProperty>10</someProperty>
    //   </values>
    // </container>
    // Without an explicit declaration that "values" is of Object type, this could be
    // interpreted as values being a subobject with a single property someProperty, or
    // <someProperty> being a *type name* which will be the value of the property "values".
    // Adding "Object" below causes us to write values as <values xsi:type="xsd:Object" .. >,
    // removing the ambiguity.
    var result = isc.Comm._xmlOpenTag(name, "Object", null, null, null, isRoot);

    object = isc.JSONEncoder._serialize_cleanNode(object);

    // for each key in the object
    for (var key in object) {

        if (key == null) continue;


        if (key == isc.gwtRef || key == isc.gwtModule) continue;

        // XML identifiers can't start with $ (parser crashes)
        if (key.startsWith('$')) continue;

        var value = object[key];

        // NOTE: null is a real value. undefined should be treated as null for cases where
        // a field's value is cleared.


        // if the value is a function, skip it
        // Exception - we can serialize actions by looking at function.iscAction - in this
        // case retain it
        if (isc.isA.Function(value) && !value.iscAction) continue;

        // convert the key to a string
        var keyStr = key.toString();

        var context = {
            objRefs: objRefs,
            objPath: isc.JSONEncoder._serialize_addToPath(objPath, key),
            isRoot: false
        };

        // transform the value
        result = isc.StringBuffer.concat(
                result,
                (prefix != null ? isc.StringBuffer.concat("\r", prefix, "\t") : ""),
                isc.Comm._xmlSerialize(keyStr, value,
                                       (prefix != null ? isc.StringBuffer.concat(prefix, "\t") : null),
                                       context)
                );
    }

    // close xml tag
    result = isc.StringBuffer.concat(
            result,
            (prefix != null ? isc.StringBuffer.concat("\r", prefix) : ""),
            isc.Comm._xmlCloseTag(name)
            );

    return result;
},

_getPrefix : function (prefixes, namespace) {
    if (prefixes[namespace] != null) {
        // re-use a declared prefix
        return prefixes[namespace];
    } else {
        // establish a new NSURI -> prefix mapping
        if (prefixes._nsCount == null) prefixes._nsCount = 0;
        return (prefixes[namespace] = "ns" + prefixes._nsCount++);
    }
},


// helper method - returns an xml open tag with the (optional) type.
_xmlOpenTag : function (tagName, type, namespace, prefix, leaveOpen, isRoot) {

    var output = isc.SB.create();

    var writeNamespace = namespace != null;

    // if "prefix" is passed as an object, use it to accrue a map from namespace to namespace
    // prefix, but don't actually write out any namespaces, relying on the calling code to do
    // so
    if (namespace != null && isc.isAn.Object(prefix)) {
        writeNamespace = false;
        prefix = this._getPrefix(prefix, namespace);
    }

    // encode the name in '_isc_name' if it's not a valid XML identifier
    var extraXML = '';
    if (!this._isValidXMLIdentifier(tagName)) {
        extraXML = ' _isc_name="' + isc.makeXMLSafe(tagName) + '"';
        tagName = "Object";
    }

    if (namespace) {
        prefix = prefix || "schNS";
        output.append("<", prefix, ":", tagName);
        if (writeNamespace) output.append(" xmlns:", prefix, "=\"", namespace, "\"");
    } else {
        output.append("<", tagName);
    }
    if (extraXML) output.append(extraXML);

    // if the object is root-level, we add the xsi namespace declaration to
    // allow usage of xsi types inline
    if (isRoot && !this.omitXSI) {
        output.append(" xmlns:xsi=\"http://www.w3.org/2000/10/XMLSchema-instance\"");
    }

    // if an xsi type is passed in for this object, mark the object with that type
    if (type && !this.omitXSI) {
        output.append(" xsi:type=\"xsd:", isc.makeXMLSafe(type), "\"");
    }

    if (!leaveOpen) output.append(">");

    return output.toString();
},

// helper method - returns an xml close tag
_xmlCloseTag : function (name, namespace, prefix) {

    if (namespace != null && isc.isAn.Object(prefix)) {
        prefix = this._getPrefix(prefix, namespace);
    }

    if (!this._isValidXMLIdentifier(name)) name = "Object";

    if (namespace) {
        prefix = prefix || "schNS";
        return isc.SB.concat("</", prefix, ":", name, ">");
    } else {
        return isc.SB.concat("</", name, ">");
    }
},

// helper method - returns the passed in value verbatim, sandwiched between the outputs of
// _xmlOpenTag and _xmlClosetTag methods with the optional type.
_xmlValue : function (name, value, type, namespace, prefix) {
        if (type == "base64Binary") {
            value = "<xop:Include xmlns:xop=\"http://www.w3.org/2004/08/xop/include\" href=\""
                + value + "\"/>";
        }
        if (type == "nil") {
            return isc.Comm._xmlOpenTag(name, null, namespace, prefix, true)
                        + " xsi:nil=\"true\"/>";

        }
        return isc.StringBuffer.concat(
                isc.Comm._xmlOpenTag(name, type, namespace, prefix),
                value,
                isc.Comm._xmlCloseTag(name, namespace, prefix)
                );
}

});    // END isc.addMethods(isc.Comm, {})



//> @type XMLDocument
// XMLDocument is the "parsed" or object form of XML, which allows XML to be navigated as
// a tree of nodes with attributes, namespaces and other metadata, as opposed to being
// manipulated as just a String.
// <P>
// XMLDocument is a native object supplied directly by the browser.  The SmartClient-supported
// interfaces for this object are methods that take an XMLDocument as an argument (such as
// +link{XMLTools.selectNodes()}).  If you want to retrieve XML data and display it in a
// SmartClient component, read about +link{group:clientDataIntegration,XML Data Binding}.  To
// extract data as JavaScript Objects from XML, see +link{XMLTools.toJS()}.  Direct
// manipulation of XMLDocument is subject to cross-browser inconsistencies, bugs, memory leaks
// and performance issues.
//
// @visibility xmlBinding
//<

//> @type XMLElement
// An XMLElement represents one complete XML tag, including any subelements contained between
// the start and end tags.
// <P>
// XMLElement is a native object supplied directly by the browser.  The SmartClient-supported
// interfaces for this object include methods that take an XMLElement as an argument (such as
// +link{XMLTools.selectNodes()}).  If you want to retrieve XML data and display it in a
// SmartClient component, read about +link{group:clientDataIntegration,XML Data Binding}.  To
// extract data as JavaScript Objects from XML, see +link{XMLTools.toJS()}.  Direct
// manipulation of XMLElements objects is subject to cross-browser inconsistencies, bugs,
// memory leaks and performance issues.
//
// @visibility xmlBinding
//<




isc.defineClass("XMLDoc").addMethods({
    addPropertiesOnCreate:false,
    init : function (xmlDoc, namespaces) {
        this.nativeDoc = xmlDoc;
        this.namespaces = namespaces;
        // the most common property access
        this.documentElement = this.nativeDoc.documentElement;
    },
    hasParseError : function () {
        if (isc.Browser.isIE) {
            var parseError = this.nativeDoc.parseError;
            // IE8 now returns zero for no error
            return parseError != null && parseError != 0;
        }
        return this.nativeDoc.documentElement &&
                this.nativeDoc.documentElement.tagName == "parsererror"; //FF
    },
    addNamespaces : function (namespaces) {
        this.namespaces = this._combineNamespaces(namespaces);
        // HACK: in the Comm watcher, for experimenting with XPath selection against XML
        // replies, it's key that any namespaces added to the XMLDoc are available.
        if (this._responseID) {
            var xmlResponse = isc.xml.xmlResponses.find("ID", this._responseID);
            if (xmlResponse) xmlResponse.xmlNamespaces = this.namespaces;
            //this.logWarn("looked up response: " + this._responseID +
            //             " and grabbed namespaces : " + this.echo(this.namespaces));
        }
    },
    _combineNamespaces : function (namespaces) {
        if (namespaces == null) return this.namespaces;
        if (this.namespaces == null) return namespaces;
        return isc.addProperties({}, this.namespaces, namespaces);
    },
    // convenience methods
    selectNodes : function (xPath, namespaces, single) {
        return isc.xml.selectNodes(this.nativeDoc, xPath,
                                   this._combineNamespaces(namespaces), single);
    },
    selectString : function (xPath, namespaces) {
        return isc.xml.selectString(this.nativeDoc, xPath,
                                    this._combineNamespaces(namespaces));
    },
    selectNumber : function (xPath, namespaces) {
        return isc.xml.selectNumber(this.nativeDoc, xPath,
                                    this._combineNamespaces(namespaces));
    },
    selectScalar : function (xPath, namespaces, asNumber) {
        return isc.xml.selectScalar(this.nativeDoc, xPath,
                                    this._combineNamespaces(namespaces), asNumber);
    },
    selectScalarList : function (xPath, namespaces) {
        return isc.xml.selectScalarList(this.nativeDoc, xPath,
                                        this._combineNamespaces(namespaces));
    },

    // passthroughs (consider writePassthroughFunctions() if this expands)
    getElementById : function (id) { return this.nativeDoc.getElementById(id) },
    getElementsByTagName : function (tagName) {
        return this.nativeDoc.getElementsByTagName(tagName)
    }
});
isc.XMLDoc.getPrototype().toString = function () {
    return "[XMLDoc <" + this.documentElement.tagName + ">]";
};

//> @class XMLTools
// Utility methods for dealing with XML elements, XML Schema, WSDL files, XSLT, and other
// XML-related functionality.
//
// @treeLocation Client Reference/Data Binding
// @visibility external
//<
isc.defineClass("XMLTools").addClassProperties({
    httpProxyURL: "[ISOMORPHIC]/HttpProxy"
});

isc.XMLTools.addClassMethods({

// Retrieval and Parsing
// ---------------------------------------------------------------------------------------



//> @classMethod XMLTools.loadXML()
// Load an XML document from the origin server or from a foreign server by relaying through the
// origin server.  An asynchronous callback provides both the XML document and raw text of the
// response.
// <P>
// Relaying through the origin server requires that the ISC HttpProxyServlet be installed and
// accessible.
//
// @param URL      (URL)       URL to load the schema from
// @param callback (callback)  callback to fire when the XML is loaded.  Signature is
//                             callback(xmlDoc, xmlText)
// @param [requestProperties] (RPCRequest) additional properties to set on the RPCRequest
//                                         that will be issued
//
// @visibility external
//<
loadXML : function (url, callback, requestProperties) {
    requestProperties = requestProperties || {};
    requestProperties.operationType = requestProperties.operationType || "loadXML";

    this.getXMLResponse(isc.addProperties({
        actionURL : url,
        httpMethod:"GET",
        callback:callback
    }, requestProperties));
},

// getXMLResponse: like rpc.sendProxied(), but parses the result as XML and gives an
// XML-specific callback of (xmlDoc,xmlText)
getXMLResponse : function (request) {
    // do an indirect callback
    request._xmlIndirectCallback = request.callback;
    request.callback = { target : this, methodName : "_getXMLResponseReply" };

    // default to POST
    request.httpMethod = request.httpMethod || "POST";

    this.logInfo("loading XML from: " + request.actionURL, "xmlComm");
    isc.rpc.sendProxied(request);
},

xmlResponses : [],
_nextResponseID : 0,
// Undocumented attribute to handle the case where we check for an empty (whitespace-only)
// response. We don't want to "trim" a massive XML doc which could be expensive, so
// avoid the call if the text exceeds this length.
trimEmptyXMLThreshold:100,
_getXMLResponseReply : function (rpcResponse, data, rpcRequest) {
    // if we already have a structured response don't try to parse it as XML
    // XXX - unless it's a RestRequest, in which case this is normal (the structured response
    // has been broken up into response specific XML substrings by RPCManager._performTransactionReply)
    // This can occur if an operation timed out
    if (rpcResponse.isStructured && !rpcRequest.isRestRequest) {
        this.fireCallback(rpcRequest._xmlIndirectCallback,
                      // NOTE: request/response probably only for internal callers
                      "xmlDoc,xmlText,rpcResponse,rpcRequest",
                      [null,null,rpcResponse,rpcRequest]);
        return;
    }

    var xmlText = rpcRequest.isRestRequest ? rpcResponse.results : rpcResponse.httpResponseText,
        xmlDoc = this.parseXML(xmlText);

    if (this.logIsInfoEnabled("xmlComm")) {
        this.logInfo("XML reply with text: " +
                     (this.logIsDebugEnabled("xmlComm") ? xmlText : this.echoLeaf(xmlText)),
                    "xmlComm");
    }

    // if the parsedXML failed parsing, set a status of -1 and fire callback
    // Exception - allow a totally empty response to continue - this is useful for
    // REST dataSources where the request kicks off server logic but the service doesn't
    // actually return meaningful response text.
    var isEmptyResponse = false;
    if (xmlText == null ||
        (xmlText.length < this.trimEmptyXMLThreshold && xmlText.trim() == ""))
    {
        isEmptyResponse = true;
    }
    if(!isEmptyResponse && xmlDoc.getElementsByTagName("parsererror").length>0) {
        rpcResponse.status = -1;
    } else {
        // retain last 5 responses in an Array for programmatic debugging
        var responses = this.xmlResponses;

        // NOTE: with the log window permanently open, you only need to enable xmlComm to catch
        // comm responses before page load
        var logViewer = isc.Log.logViewer;
        if (this.logIsDebugEnabled("xmlComm") ||
            (isc.Page.isLoaded() && logViewer && logViewer.logWindowLoaded()))
        {
            var responseID = this._nextResponseID++;
            responses.add({
                ID : responseID,
                text : xmlText
            });
            // HACK: label the XMLDoc created from this xmlText with the id of the response - this
            // allows the XMLDoc to tack xml namespaces onto the response later
            if (xmlDoc) xmlDoc._responseID = responseID;

            // keep a limited number of responses
            if (responses.length > 10) responses.shift();

            // update log window if showing
            if (logViewer && logViewer.logWindowLoaded() && logViewer._logWindow != null) {
                logViewer._logWindow.updateCommWatcher();
            }
        } else {
            responses.length = 0;
        }
    }
    this.fireCallback(rpcRequest._xmlIndirectCallback,
                      // NOTE: request/response probably only for internal callers
                      "xmlDoc,xmlText,rpcResponse,rpcRequest",
                      [xmlDoc,xmlText,rpcResponse,rpcRequest]);
},



//                    IE6                   IE5.5                we used to use this
xmlDOMConstructors : ["MSXML2.DOMDocument", "MSXML.DOMDocument", "Microsoft.XMLDOM"],


//> @classMethod XMLTools.parseXML()
// Parse XML text into an +link{XMLDocument}.  Parse errors, if any, are reported to the log.
//
// @param xmlText (String) XML text to be parsed
// @return (XMLDocument) resulting XMLDocument
// @visibility external
//<

mozAnchorBug : isc.Browser.isMoz && (isc.Browser.geckoVersion < 20080205)
                && window.location.href.indexOf("#") != -1,
parseXML : function (xml, suppressErrors) {
    if (xml == null) return; // Moz will actually throw exceptions in this case

    xml = this.trimXMLStart(xml);

    var doc;
    if (!isc.Browser.isIE) {
        try {

            if ((this.mozAnchorBug || this.useAnchorWorkaround) &&
                this.useAnchorWorkaround !== false)
            {
                var iframeHTML = "<IFRAME STYLE='position:absolute;visibility:hidden;top:-1000px'"
                                +" ID='isc_parseXMLFrame'></IFRAME>";
                if (!isc.Page.isLoaded()) {
                    document.write(iframeHTML);
                } else {
                    isc.Element.insertAdjacentHTML(document.getElementsByTagName("body")[0], "beforeEnd", iframeHTML)
                }
                var iframe = document.getElementById("isc_parseXMLFrame");
                var iframeWindow = iframe.contentWindow;


                window.isc.xmlSource = xml;
                iframeWindow.location.href = "javascript:top.isc.parsedXML="
                    + "new top.isc.XMLTools.getXMLParser().parseFromString(top.isc.xmlSource, 'text/xml')";
                doc = window.isc.parsedXML;

                isc.xmlSource = isc.parsedXML = null;

                // remove the iframe
                iframe.parentNode.removeChild(iframe);
            } else {
                doc = this.getXMLParser().parseFromString(xml, "text/xml");
            }
        } catch (e) {
            if (!suppressErrors) this._logParseError(this.echo(e));
            return null;
        }

        if (!suppressErrors && doc.documentElement &&
             doc.documentElement.tagName == "parsererror")
        {
            this._logParseError(doc.documentElement.textContent);
            return null;
        }

        return isc.XMLDoc.create(doc);
    }

    doc = this.getXMLParser();
    if (!doc) {
        this._warnIfNativeXMLUnavailable("XMLTools.parseXML()");
        return;
    }

    doc.loadXML(xml);
    if (doc.parseError != 0) {
        var error = doc.parseError;
        if (!suppressErrors) {
            this._logParseError(
                "\rReason: " + error.reason +
                "Line number: " + error.line + ", character: " + error.linepos +
                "\rLine contents: " + error.srcText +
                (!isc.isA.emptyString(error.url) ? "\rSource URL: " + error.url : ""));
        }
        return null;
    }
    return isc.XMLDoc.create(doc);
},

// NOTE: don't obfuscate "trimXMLStart", used by dev console
trimXMLStart : function (xml) {


    if (xml.indexOf("<?xml") != -1)
    {

        var match = xml.match(new RegExp("^\\s*<\\?[^?]*\\?>"));
        if (match) {
            xml = xml.substring(match[0].length);
            //this.logWarn("match is: " + this.echoAll(match) + ", trimming by: " + match[0].length);
        }
    }


    if (isc.Browser.isIE && xml.indexOf("<!DOCTYPE") != -1) {
        var match = xml.match(new RegExp("^\\s*<!DOCTYPE .*>"));
        if (match) {
            xml = xml.substring(match[0].length);
            //this.logWarn("match is: " + this.echoAll(match) + ", trimming by: " + match[0].length);
        }
    }
    return xml;
},

_logParseError : function (errorText, xml) {
    this.logWarn("Error parsing XML: " + errorText +
                 (this.logIsDebugEnabled("parseXML") ?
                  "\rXML was:\r" + xml + "\rTrace:" + this.getStackTrace() : ""),
                 "parseXML");
},


getXMLParser : function () {
    // Moz/Firefox, Safari support a native DOMParser
    if (!isc.Browser.isIE) {
        if (!this._parser) this._parser = new DOMParser();
        return this._parser;
    }

    var parser;
    if (this._xmlDOMConstructor) {
        parser = new ActiveXObject(this._xmlDOMConstructor);
    } else {
        for (var i = 0; i < this.xmlDOMConstructors.length; i++) {
            try {
                var cons = this.xmlDOMConstructors[i];
                parser = new ActiveXObject(cons);
                if (parser) {
                    this.logInfo("Using XML DOM constructor: " + cons);
                    this._xmlDOMConstructor = cons;
                    break;
                }
            } catch (e) { }
        }
        if (!parser) {
            this.logWarn("Couldn't create XML DOM parser - tried the following"
                         +" constructors: "+this.echoAll(this.xmlDOMConstructors));
        }
    }
    return parser;
},

//> @classMethod XMLTools.nativeXMLAvailable()
//
// Returns true if the current browser exposes an XML parser that can be used for SmartClient
// XML operations like web service bindings and XML processing.  See
// +link{group:platformDependencies} for more information on when the XML parser may not
// available and what features are impacted as a result.
//
// @return (boolean) true if native XML processing is available, false otherwise.
//
// @visibility external
//<
nativeXMLAvailable : function () {

    if (isc.Browser.isSafari && !isc.Browser.isApollo && (isc.Browser.safariVersion < 522))
        return false;
    return this._parser != null || this.getXMLParser() != null;
},


_warnIfNativeXMLUnavailable : function (featureName) {
    if (this.nativeXMLAvailable() || !this.logIsWarnEnabled()) return false;

    var message = "Feature " + featureName + " requires a native XML parser which is "
                  + "not available ";
    if (isc.Browser.isSafari) {
        message += "because this version of Safari does not support native XML processing.";
    } else {
        // IE ActiveX
        message += "because ActiveX is currently disabled.";
    }
    message += " Please see the 'Features requiring ActiveX or Native support'"
              +" topic in the client-side reference under Client Reference/System"
              +" for more information";
    this.logWarn(message);
    return true;
},

// Access / serverToJS
// ---------------------------------------------------------------------------------------

// call the server to do an XML2JS conversion, evals result
serverToJS : function (xmlString, callback, evalVars) {
    isc.DMI.callBuiltin({
        methodName: "xmlToJS",
        callback: callback,
        arguments: xmlString,
        requestParams: {
            evalVars: evalVars,
            evalResult: true
        }
    });
},

// calls the server to do an XML2JS conversion, returns string as 'data' in callback
toJSCode : function (xmlString, callback) {
    isc.DMI.callBuiltin("xmlToJS", xmlString, callback);
},


// convert an XML or DOM element to a JavaScript object.  Attributes and subelements become
// properties.  Note: if nodeNames collide, last one wins.  Also note that getElementsByTagName(*)
// returns all children nodes recursively - not just the immediate children.  So you get a flat
// representation of a potentially nested object.  See the more expensive toJS() for a nested
// result.
_$star : "*",
elementToObject : function (element) {
    if (element == null) return null;

    var object = this.getAttributes(element);

    // transform subelements to properties
    var children = element.getElementsByTagName(this._$star);
    for (var i = 0; i < children.length; i++) {
        var child = children[i];
        object[child.tagName] = this.getElementText(child);
    }

    return object;
},

_$colon : ":",
// return the non-namespace-prefixed tagName, or #text / #comment / #cdata-section for text
// nodes
getLocalName : function (node) {
    // All: tagName/nodeName prefixed
    // Moz: localName is non-prefix'd
    if (!isc.Browser.isIE) {
        var localName = node.localName;
        if (localName == null) return node.nodeName; // for #text nodes in Moz
        return localName;
    }


    var name = node.nodeName,
        colonIndex = name.indexOf(this._$colon);
    if (colonIndex != -1) return name.substring(colonIndex + 1);
    return name;
},

//> @classMethod XMLTools.toJS()
// Translates an XML fragment to JavaScript collections.  This method works just like the
// server-side method XML.toJS(Element, Writer):
// <ul>
// <li>  Elements become JavaScript Objects with each attribute becoming a property
// <li>  Subelements with just text (no child elements or attributes) become properties
// <li>  Subelements with child elements or attributes become sub objects
// </ul>
// For example, if you pass the following fragment to this method:
// <pre>
// &lt;foo&nbsp;bar="zoo"&gt;
//     &nbsp;&nbsp;&lt;x&gt;y&lt;/x&gt;
// &lt;/foo&gt;
// </pre>
// You will get back the following JS structure:
// <pre>
// { bar:"zoo", x:"y"}
// </pre>
// All atomic property values will be of String type.  Use +link{DataSource.recordsFromXML()}
// to do schema-driven XML to JS transform, which can produce correctly typed values.
//
// @param element (XMLElement or XMLDocument) The element to transform to JS
// @return (Object) The resulting JavaScript collection.
//
// @visibility external
// @example xmlServerValidationErrors
//<
// deep transform XML to JS.  calls itself recursively.  subelements with just the value get
// collapsed, key collisions are turned into arrays.  There are almost certainly still
// differences from server-side transform but none are known at this time.
//
// You can supply an array of fieldNames to look for ("attrMask") - this limits the output data
// to those fields, and is substantially faster in IE for an XML representation that uses lots
// of attributes rather than subelements
_$List : "List",
_$xmlToJS : "xmlToJS",
toJS : function (element, attrMask, dataSource, widgetXML, context) {
    if (element == null) return null;
    if (isc.isAn.XMLDoc(element)) element = element.nativeDoc;
    if (element.documentElement) element = element.documentElement;
    context = context || isc.emptyObject;

    // handle arrays of elements
    if (isc.isAn.Array(element)) {
        var results = [];
        for (var i = 0; i < element.length; i++) {
            results[i] = this.toJS(element[i], attrMask, dataSource, widgetXML, context);
        }
        return results;

    }

    var object,
        fieldNames;

    // check explicit type

    var type = this.getExplicitType(element, widgetXML);
    if (widgetXML || !dataSource || (dataSource && isc.DS.get(type) == null)) {


        if (widgetXML) {
            // for widgetXML, detect <Canvas ref="someId"/>, the official way of referring to other
            // components
            var refId = this.isRefElement(element);
            if (refId)
            {
                var canvas = isc.Canvas.getById(refId);
                //this.logWarn("ref: " + refId + ", found related Canvas: " + canvas);
                if (canvas != null) return canvas;
            }
            // detect indirect refs <Canvas ref="someId"/>.  Eg if this schema is Menu.ds.xml
            // invoked from Canvas.contextMenu, we're being invoked on the <contextMenu>
            // element, and we need to check if it has a single child <Menu ref="someId"/>
            var firstChild = this.firstElementChild(element),
                refId = firstChild ? this.isRefElement(firstChild) : null;
            if (refId && this.getElementChildren(element).length == 1)
            {
                var canvas = isc.Canvas.getById(refId);
                //this.logWarn("ref: " + refId + ", found related Canvas: " + canvas);
                if (canvas != null) return canvas;
            }

            if (!type) {
                // in widgetXML mode, if there's no explicitly specified type on the element,
                // but the tag name matches any known complexType, treat this as a type
                // override.  Allows eg <ListGrid> to override the "members" field type being
                // "Canvas"
                var tagName = element.tagName;
                if (tagName == this._$List || isc.DS.get(tagName)) type = element.tagName;
            }
        }


        // handle like field.multiple=true if List type explicitly specified.  In widgetXML
        // mode this includes a tag of <List>.
        if (type != null && type == this._$List) {
            var children = this.getElementChildren(element);
            return this.toJS(children, attrMask, dataSource, widgetXML, context);
        }

        // otherwise if explicit type is set (to something we recognize), honor it
        if (type) {
            if (isc.DS.get(type) != null) {
                // NOTE: this case currently occurs only for widgetXML mode and schemaless
                // mode, otherwise, you don't get into this block
                dataSource = isc.DS.get(type);
            } else if (isc.SimpleType.getType(type) != null) {
                // known simple type
                return isc.SimpleType.validateValue(type, this.getElementText(element));
            }
        }
    }

    // respect custom xmlToJS transform
    if (dataSource && dataSource.xmlToJS) return dataSource.xmlToJS(element, context);

    if (this.elementIsNil(element)) return null;

    // if a dataSource was passed, ask the DataSource to derive the field values for any field
    // that defines a valueXPath or getFieldValue function
    if (dataSource) {

        fieldNames = attrMask || dataSource.getFieldNames();
        object = {};
        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = dataSource.getField(fieldName);

            if (field == null ||
                (field.valueXPath == null && field.getFieldValue == null)) continue;

            var value = dataSource.getFieldValue(element, fieldName, field);
            if (value != null) {
                if (this.logIsDebugEnabled(this._$xmlToJS)) {
                    this.logDebug("valueXPath / getFieldValue() field: " +
                                  dataSource.ID + "." + fieldName +
                                  " on element: " + this.echoLeaf(element) +
                                  " got value: " + value, "xmlToJS");
                }
                object[fieldName] = value;
            }
        }
        //this.logWarn("object is now: " + this.echo(object));
    }

    // get attributes (or if a dataSource was passed, add to existing object without
    // clobbering)
    object = this.getAttributes(element, attrMask, object, dataSource != null, dataSource);

    // no attributes, no element children: convert to simple value
    if (!this._hasDataAttributes(object) && !this.hasElementChildren(element))
    {
         return this.getElementText(element);
    }


    if (object[this._$xsiType] && object[this._$xsiType] == "xsd:Object") {
        delete object[this._$xsiType];
    }

    // transform subelements to properties
    var children = element.childNodes;

    if (this.logIsDebugEnabled(this._$xmlToJS)) {
        this.logDebug("using DataSource: " + dataSource +
                      " for complex element: " + this.echoLeaf(element) +
                      " childNodes: " + this.echoLeaf(children) +
                      " has attributes: " + this._hasDataAttributes(object)
                      ,"xmlToJS");
    }

    var hadElementChildren = false;
    for (var i = 0; i < children.length; i++) {
        var child = children[i];

        // we want the non-namespaced name
        var childName = this.getLocalName(child);

        // skip text nodes, which in FF we get every time the source XML has CRs
        if (this.isTextNode(child)) continue;

        hadElementChildren = true;

        // if we have an attrMask, ignore child elements not listed in the attrMask
        if (attrMask && !attrMask.contains(childName)) continue;

        var field = dataSource ? dataSource.getField(childName) : null;
        // if there was a field defined with this name that had a valueXPath or getFieldValue
        // function, ignore the subelement
        if (field && (field.valueXPath || field.getFieldValue)) continue;

        var childValue;
        if (this.logIsInfoEnabled(this._$xmlToJS)) {
            this.logInfo("dataSource: " + dataSource +
                         ", field: " + this.echoLeaf(field) +
                         (field != null ? " type: " + field.type : "") +
                         ", XML element: " + this.echoLeaf(child), "xmlToJS");
        }

        // field.multiple means the immediate child is just a container tag indicating
        // the property name - the indirect children are the elements that actually
        // conform to the declared type and provide the value
        var fieldValue = child;
        if (field && field.multiple) {
            // use the indirectChildren as the value, unless they're aren't any
            var indirectChildren = this.getElementChildren(child);
            if (indirectChildren.length > 0) fieldValue = indirectChildren;
        }


        if (!dataSource || field == null || field.type == null || field.type == "any") {
            if (this.logIsDebugEnabled(this._$xmlToJS)) {
                this.logDebug("applying schemaless transform at: " +
                              (dataSource ? dataSource.ID : "[schemaless]") + "." + childName,
                              "xmlToJS");
            }
            // if we have no source of type, perform recursive schemaless transform.
            // Note we'll do this even if we have a field, but the field is typeless (the field
            // may, eg, specify multiple:true)
            childValue = this.toJS(fieldValue, null, null, widgetXML, context);
        } else {

            var childSchema = dataSource.getSchema(field.type);
            // if the field is of dataSource type, do recursive transform
            if (childSchema != null) {
                var childContext = field.propertiesOnly ? {propertiesOnly: true} : context;
                childValue = this.toJS(fieldValue, null, childSchema, widgetXML, childContext);

                if (this.logIsDebugEnabled(this._$xmlToJS)) {
                    this.logDebug("complexType field: " + this.echoLeaf(field) +
                                  " got value: " + this.echoLeaf(childValue),
                                  "xmlToJS");
                }
            // otherwise, get the simple value
            } else {
                // NOTE: there's no way to use eg xsi:type to override an explicitly declared
                // simple type on a field (no known use case for this however)

                // field is multiple:true
                if (isc.isAn.Array(fieldValue)) {
                    childValue = [];
                    for (var j = 0; j < fieldValue.length; j++) {
                        childValue.add(
                            dataSource.validateFieldValue(field,
                                                          this.getElementText(fieldValue[j]))
                        );
                    }
                } else {
                    childValue = dataSource.validateFieldValue(field,
                                                               this.getElementText(fieldValue));
                }

                if (this.logIsDebugEnabled(this._$xmlToJS)) {
                    this.logDebug("simpleType field: " + this.echoLeaf(field) +
                                  " got value: " + this.echoLeaf(childValue),
                                  "xmlToJS");
                }
            }
        }

        if (field && field.multiple) {
            // null or the empty string, the result of a nil or empty element
            // respectively, should be considered an empty array for a multiple field
            if (childValue == null || isc.isA.emptyString(childValue)) childValue = [];
            // anything else should be wrapped in an Array if it's singular
            else if (!isc.isAn.Array(childValue)) childValue = [childValue];
        }

        // collision on the tagName - make an array
        if (object[childName]) {
            if (!isc.isAn.Array(object[childName])) object[childName] = [object[childName]];

            if (field && field.multiple && isc.isAn.Array(childValue)) {
                // for "multiple" fields, avoid nested Arrays
                object[childName].addList(childValue);
            } else {
                object[childName].add(childValue);
            }
        } else {
            object[childName] = childValue;
        }
    }

    // for an element that has no element children, store it's textContent, if any, as a
    // special property.  Note that we still drop text content around element children, but
    // this is typically either whitespace/CRs used for formatting, or the relative order of
    // the text and subelements matters and hence a very different representation would be
    // needed.
    if (!hadElementChildren) {
        var textContent = this.getElementText(element),
            prop = context.textContentProperty ||
                      (dataSource ? dataSource.textContentProperty : "xmlTextContent");

        // if there's a field definition for textContent, validate against it
        if (dataSource) {
            field = dataSource.getTextContentField();
            //this.logWarn("validating against textContentField: " + this.echoLeaf(field) +
            //             ", with validators: " + this.echoAll(field.validators));
            if (field) textContent = dataSource.validateFieldValue(field, textContent);
        }
        if (textContent != null && !isc.isAn.emptyString(textContent)) {
            object[prop] = textContent;
        }
    }


    // if we have a dataSource with a instanceConstructor property that maps to an existing
    // class, create a new one of those with the data we've mined off the XML

    if (widgetXML) {
        if (!object._constructor && dataSource && (dataSource.instanceConstructor || dataSource.Constructor)) {
            object._constructor = dataSource.instanceConstructor || dataSource.Constructor;
        }

        if (!object.scClassName && dataSource && dataSource.scClassName) {
            object.scClassName = dataSource.scClassName;
        }

        if (
            object._constructor &&
            (context == null || !context.propertiesOnly) &&
            isc.ClassFactory.getClass(object._constructor)
        ) {
            var Constructor = object._constructor;
            delete object._constructor;
            //this.logWarn("toJS creating an instance of: " + Constructor +
            //             " with properties: " + isc.echo(object));
            return isc.ClassFactory.newInstance(Constructor, object);
        }
    }

    return object;
},

// copied from BasicDataSource.getExplicitType() in server code
_$type : "type",
_$xsiType : "xsi:type",
getExplicitType : function (element, widgetXML) {
    if (element == null || this.isTextNode(element)) return;

    var type = this.getXSIAttribute(element, this._$type);
    if (type) {
        if (type.contains(isc.colon)) type = type.substring(type.indexOf(isc.colon)+1);
    } else if (widgetXML) {
        type = element.getAttribute("constructor");

        // Only honor constructor as a type if there is actually a DataSource
        // for it. Otherwise, specifying <ListGrid constructor="SomeSubclass" />
        // doesn't work property, because the attributes don't get validated
        // against the ListGrid type.
        if (type && !isc.DS.get(type)) type = null;
    }

    return type;
},

// for widgetXML, detect <Canvas ref="someId"/>, the official way of referring to other
// components
_$ref:"ref",
isRefElement : function (element) {
    if (element == null || this.isTextNode(element)) {
        return false;
    }
    var refId = element.getAttribute(this._$ref);
    if (refId && element.attributes.length == 1 && !this.hasElementChildren(element)) return refId;
},

// converts isomorphic:XML to components, complains about missing system schema
toComponents : function (xmlDoc, context) {

    if (isc.DS.get("Canvas") == null) {
        this.logWarn("Can't find schema for Canvas - make sure you've loaded"
                     +" component schema via <isomorphic:loadSystemSchema/> jsp tag"
                     +" or by some other mechanism");
    }

    // accept string or xml document
    if (isc.isA.String(xmlDoc)) {
        // accept non-well-formed documents that are just a bunch of ISC components with no
        // containing <isomorphicXML> tag by wrapping the components in an <isomorphicXML> tag
        // if initial parse run fails
        var doc = this.parseXML(xmlDoc, true);
        if (doc.hasParseError()) {
            this.logWarn("xml failed to parse xmlDoc, wrapping in root node");
            doc = this.parseXML("<isomorphicXML>"+xmlDoc+"</isomorphicXML>");
        }
        xmlDoc = doc;
    }

    return this.toJS(xmlDoc, null, null, true, context);
},


_$number : "number",
getFieldValue : function (record, fieldName, field, dataSource, namespaces) {

    if (record.ownerDocument == null) return record[fieldName];

    // if a field is passed, it's from a UI component, and is the complete field definition
    // whether the UI component was databound or not because databound components merge their
    // fields against dataSource fields
    field = field || (dataSource ? dataSource.getField(fieldName) : isc.emptyObject);

    try {
        var value;
        if (field.valueXPath) {
            // if the field is of DataSource type, use the valueXPath to select nodes (not a
            // scalar value), and do recursive transform on those elements
            var fieldDS = (dataSource ? dataSource.getSchema(field.type) :
                                        isc.DS.get(field.type));
            if (fieldDS) {
                var elements = isc.xml.selectNodes(record, field.valueXPath, namespaces),
                    records = isc.xml.toJS(elements, null, fieldDS);

                // selectNodes always returns an Array, but we only want an Array value if the
                // field is declared multiple or if multiple nodes really did match.
                if (!field.multiple && records.length == 1) records = records[0];
                return records;
            } else {
                // otherwise simple scalar value
                value = isc.xml.selectScalar(record, field.valueXPath, namespaces);
            }
        } else {
            value = isc.xml.getXMLFieldValue(record, fieldName);
        }
        // the value retrieved from XML is always just a string, so we need to convert it
        // to the proper type.  NOTE: call instance method to allow overrides.
        dataSource = dataSource || isc.DS.get("Object");
        value = dataSource.validateFieldValue(field, value);
        //this.logWarn("At field: " + dataSource.ID + "." + field.name +
        //             " got value: " + this.echoLeaf(value));
        return value;
    } catch (e) {
       this.logWarn("error getting value for field: '" + fieldName +
                    (field.valueXPath ? "', valueXPath: '" + field.valueXPath : "") +
                    "' in record: " + this.echo(record) +
                    "\r: " + this.echo(e) + this.getStackTrace());
       return null;
    }
},

// given an XML element, get the value for the given fieldName by checking attributes, then
// subelements.  Returned value is always a String or null.
getXMLFieldValue : function (element, fieldName) {
    // attribute representation

    var attrValue = element.getAttribute(fieldName);
    if (attrValue != null) return attrValue;

    // subElement representation
    // NOTE: a tagName of "record" matches <foo:record> in Moz, but NOT in IE
    var subElement = element.getElementsByTagName(fieldName)[0];
    if (subElement == null) return null;
    return (isc.Browser.isIE ? subElement.text : subElement.textContent);
},

// whether the element has any attributes that should be considered data, as opposed to
// encoding directives like "xsi:type"
_hasDataAttributes : function (attributes) {
    for (var attrName in attributes) {
        if (attrName == this._$xsiType) continue;
        return true;
    }
    return false;
},


_$xmlnsColon: "xmlns:",
getAttributes : function (element, attrMask, object, dontClobber, dataSource) {
    // NOTE: hasAttributes() doesn't exist in IE

    // optionally add to existing object
    object = object || {};

    var undef;

    // look up the attributes specified by attrMask
    if (attrMask) {
        if (!isc.isAn.Array(attrMask)) attrMask = [attrMask];
        for (var i = 0; i < attrMask.length; i++) {
            var attrName = attrMask[i];
            if (dontClobber && object[attrName] !== undef) continue;
            var attrValue = element.getAttribute(attrName);
            if (attrValue == null || isc.isAn.emptyString(attrValue)) continue;

            if (dataSource && dataSource.getField(attrName)) {
                attrValue = dataSource.validateFieldValue(dataSource.getField(attrName),
                                                          attrValue);
            }
            object[attrName] = attrValue;
        }
        return object;
    }

    // transform attributes to properties
    var attrs = element.attributes;
    if (attrs != null) {
        for (var i = 0; i < attrs.length; i++) {
            var attr = attrs[i],
                attrName = attr.name;

            // Change constructor to _constructor, because constructor
            // has a special meaning for Javascript
            if (attrName == "constructor") attrName = "_constructor";

            if (dontClobber && object[attrName] !== undef) continue;

            // whether to include namespace declarations in JS data
            if (isc.startsWith(attrName, this._$xmlnsColon) &&
                dataSource && dataSource.dropNamespaceDeclarations) continue;

            var attrValue = attr.value;
            if (attrValue == null || isc.isAn.emptyString(attrValue)) continue;

            if (dataSource && dataSource.getField(attrName)) {
                attrValue = dataSource.validateFieldValue(dataSource.getField(attrName),
                                                          attrValue);
            }
            object[attrName] = attrValue;
        }
    }
    return object;
},



_$xsi : {
    nil : "xsi:nil",
    "null" : "xsi:null",
    type : "xsi:type"
},
xsiNamespaces : [
    "http://www.w3.org/2001/XMLSchema-instance",
    "http://www.w3.org/1999/XMLSchema-instance"
],
getXSIAttribute : function (element, attrName) {
    var value;
    // Opera requires that we call getAttributeNS to get the xsi elements, using e.g:
    // getAttribute("xsi:type") returns null, even if set.  Probably should use this code
    // with other browser as well, but it's slower (because we have to try all the
    // xsiNamespaces) and untested on other platforms.
    if (isc.Browser.isOpera) {
        for (var i = 0; i < this.xsiNamespaces.length; i++) {
            value = element.getAttributeNS(this.xsiNamespaces[i], attrName);
            if (value != null) return value;
        }
        return value;
    }

    return element.getAttribute(this._$xsi[attrName]);
},

_$nil : "nil",

_$null : "null",
_$false : "false",
_$zero : "0",
elementIsNil : function (element) {

    if (element == null || !isc.isA.XMLNode(element) || element.nodeType != 1) return false;


    var nilAttribute = this.getXSIAttribute(element, this._$nil);


    if (nilAttribute && nilAttribute != this._$false && nilAttribute != this._$zero) return true;

    var nilAttribute = this.getXSIAttribute(element, this._$null);
    if (nilAttribute && nilAttribute != this._$false && nilAttribute != this._$zero) return true;

    return false;
},


getElementText : function (element) {
    if (this.elementIsNil(element)) return null;
    if (!element) return null;
    var child = element.firstChild;
    if (!child) {
        // from Firefox 13 to Firefox 14 XML nodes representing attributes were changed to no
        // longer have a #text child.

        if ((isc.Browser.isMoz || isc.Browser.isChrome) && element.nodeType == 2) return element.value;
        return isc.emptyString; // empty element, but not marked nil
    }
    var text = child.data;

    if (isc.Browser.isMoz && text != null && text.length > 4000) return element.textContent;
    return text;
},

// whether an element is a text node
isTextNode : function (element) {
    if (element == null) return false;
    var nodeType = element.nodeType;
    // 3:text, 4:cdata, 8:comment
    return (nodeType == 3 || nodeType == 4 || nodeType == 8);
},

// whether an element has an element child (as opposed to only text children)
hasElementChildren : function (element) {
    return this.firstElementChild(element) != null;
},

// return the first element child (as opposed to text node child) if there is one, otherwise
// null
firstElementChild : function (element) {

    if (element == null ||
        (element.hasChildNodes != null && element.hasChildNodes() == false)) return null;

    var childNodes = element.childNodes;
    if (!childNodes) return null;
    var length = childNodes.length;
    for (var i = 0; i < length; i++) {
        var child = childNodes[i];
        if (!this.isTextNode(child)) return child;
    }
    return null;
},

// JS -> XML
// ---------------------------------------------------------------------------------------
setAttributes : function (element, values) {
    var undef;
    for (var propName in values) {
        var value = values[propName];

        if (value == null) {
            element.removeAttribute(propName);
            continue;
        }

        // setAttribute("attr", true) in IE stores "-1" (yes, seriously)
        if (isc.Browser.isIE && (value === true || value === false)) {
            value = isc.emptyString + value;
        }

        element.setAttribute(propName, values[propName]);
    }
},

// XPath
// ---------------------------------------------------------------------------------------

// make all namespaces declared on the document element available under the same prefix used in
// the document itself, and make the default namespace, if there is one, available as
// "default".  "namespaces" can be passed it to avoid redeclaration of existing prefixes.
_makeIEDefaultNamespaces : function (doc, namespaces) {

    var buffer = isc.SB.create(),
        docElement = doc.documentElement,
        namespaces = namespaces || isc.emptyObject,
        defaultNamespace;

    // if it hasn't been specified explicitly, try to figure out the default namespaces
    // NOTE: default is a keyword in IE
    if (!namespaces["default"]) {
        defaultNamespace = this._deriveDefaultNamespace(docElement);
        if (defaultNamespace) buffer.append('xmlns:default="', defaultNamespace, '" ');
    }

    // add all the namespaces on the document element
    var attrs = doc.documentElement.attributes;
    for (var i = 0; i < attrs.length; i++) {
        var attr = attrs[i],
            prefix = attr.prefix;
        // NOTE: attr.name is the full name of the attribute, including prefix, so for a
        // non-default namespace declaration attr.name will be eg xmlns:somePrefix
        if (prefix == "xmlns" && prefix != attr.name) {
            // don't redefine already defined selection namespaces
            // NOTE: baseName is IE-only
            if (namespaces[attr.baseName] != null) continue;
            buffer.append(attr.name, '="', attr.value, '" ');
        }
    }
    return buffer.toString();
},


// Method to determine the "default" namespace by looking at the namespaceURI of the document
// element or (if appropriate) children of the documentElement.
// Used in IE as part of _makeIEDefaultNamespaces()
//
// Note: This handles the common case where the document element declares a namespace, but
// is itself in a different namespace
_deriveDefaultNamespace : function (docElement) {



    var shouldLog = this.logIsDebugEnabled("xmlSelect");
    if ((docElement.prefix == null || isc.isAn.emptyString(docElement.prefix)) &&
        docElement.namespaceURI)
    {
        if (shouldLog) {
            this.logWarn("using docElement ns, prefix: " + docElement.prefix,
                              "xmlSelect");
        }
        return docElement.namespaceURI;


    } else if (docElement.firstChild) {
        var defaultNamespace
        for (var i = 0; i < docElement.childNodes.length; i++) {
            var childNode = docElement.childNodes[i];
            // text nodes show up in the childNodes collection in Safari
            if (childNode.nodeType == 3) continue;

            var nsURI = childNode.namespaceURI;
            if (!nsURI) break;

            if (childNode.prefix == null || isc.isAn.emptyString(childNode.prefix)) {
                defaultNamespace = childNode.namespaceURI;
                break;
            }
        }
        if (defaultNamespace != null) {
            if (shouldLog) {
                this.logDebug("using default namespace detected on child: " +
                              defaultNamespace, "xmlSelect");
            }
        }
        // if there is no default namespace, still define the namespace prefix "default" as
        // the document element's namespace, for conveniece
        if (defaultNamespace == null && docElement.namespaceURI) {
            defaultNamespace = docElement.namespaceURI;
            if (shouldLog) {
                this.logDebug("using document element's namespace as default namespace: " +
                              defaultNamespace, "xmlSelect");
            }
        }
        // if no appropriate default namespace could be derived, still define one, so that
        // using "default:" doesn't create a JS error.  This allows an XPath like
        // "//default:item|//item" to handle both namespaced and non-namespaced RSS feeds
        if (!defaultNamespace) defaultNamespace = "http://openuri.org/defaultNamespace";
        return defaultNamespace;
    }
},


//> @classMethod xmlTools.selectObjects()   (A)
// Applies an XPath expression to JavaScript objects, returning matching objects.
// <P>
// Both child and attribute names are interpreted as property names, and array access notation
// can be used to select elements from Arrays.  For example:<pre>
//     var results = {
//        searchResults:[
//            { title:"Page One", relevance:6.3 },
//            { title:"Page Two", relevance:5.2,
//              summary: "Summary of Page One" }
//        ]
//     };
//
//     // returns the "searchResults" two-item Array
//     isc.XMLTools.selectObjects(results, "/searchResults");
//
//     // returns the first item under "searchResults", in an Array (NOTE: in XPath, Array
//     // index starts at 1, not 0)
//     isc.XMLTools.selectObjects(results, "/searchResults[1]");
//
//     // returns ["Page One"]
//     isc.XMLTools.selectObjects(results, "/searchResults[1]/title");
//
//     // also returns ["Page One"]
//     isc.XMLTools.selectObjects(results, "/searchResults[1]@title");
// </pre>
// A limited form of XPath "predicates", that is, expressions with brackets that filter
// returned objects, is allowed.  A predicate can be either:
// <ul>
// <li> a number only, eg [5], for Array access
// <li> the XPath function call "last()", eg [last()], to retrieve the last item
// <li> a property name (*without* any leading "@"), meaning that the property contains a value
//      that is considered "true" in JavaScript.  For example: [summary]
// <li> a property name, comparison operator, and either a number or String literal, for
//      example, [name = "bob"].  In this case the property can also be the XPath function
//      position(), for example, [position() > 5]
// </ul>
// Some examples of using simple predicates with the sample data above:
// <pre>
//     // returns an Array with only the first result
//     isc.XMLTools.selectObjects(results, "/searchResults[relevance > 5.5]");
//
//     // return an Array with only the second result, since the first has no summary
//     isc.XMLTools.selectObjects(results, "/searchResults[summary]");
// </pre>
// Details of the XPath -> Objects mapping:
// <ul>
// <li> JavaScript Object properties are considered element children, and text children do not
//      exist (in the XML model, text children exist *between* element children, but nothing
//      exists between JavaScript properties)
// <li> The contents of Array-valued properties are considered immediate element children (this
//      is consistent with the predicate "[5]" acting like Array access)
// <li> "*" in XML selects all element children, so "*" in Object XPath selects the values of
//      all properties, that is, +link{classMethod:isc.getValues(),isc.getValues(object)}, except
//      that Array-valued properties are "flattened" into the returned list.
// </ul>
//
// @param object (Object) Object to select results from
// @param xPath (String) XPath expression
// @return (Array) Array of matching objects, or null for no match
// @visibility external
//<


_$leftBracket : "[",
selectObjects : function (object, xPath, singleValue) {
    if (isc.contains("|")) {
        var subExpressions = xPath.split(/|/),
            results = [];
        for (var i = 0; i < subExpressions.length; i++) {
            results.addList(this.selectObjects(subExpressions[i], object));
        }
        return results;
    }
    // canonicalize to an Array
    var objects = isc.isAn.Array(object) ? object : [object];

    if (xPath != isc.slash) {
        if (isc.startsWith(xPath, isc.slash)) {
            xPath = xPath.substring(1);

            if (isc.startsWith(xPath, isc.slash)) {
                this.logWarn("Selector \"\\\\\" does not supported. It was truncated.");
                xPath = xPath.substring(1);
            }
        }

        var segments = xPath.split(/[\/@]/);

        //this.logWarn("segments: " + this.echo(segments));

        objects = this._selectObjects(segments, objects, isc.slash);
    }

    // return the single value, or null on no match
    if (singleValue && objects.length <= 1) return objects[0];

    return objects;
},

_selectObjects : function (segments, objects, path) {

    var segment = segments[0];
    segments = segments.length > 1 ? segments.slice(1) : null;
    //this.logWarn("at path: " + path +
    //             ", applying segment: '" + segment +
    //             "' to " + this.echoLeaf(objects));

    if (objects == null) return null;

    // break segment into nodeTest and predicate
    var predicate,
        nodeTest = segment,
        bracketIndex = segment.indexOf(this._$leftBracket);

    if (bracketIndex != -1) {
        nodeTest = segment.substring(0, bracketIndex);
        // extract predicate expression (NOTE: assume one only)
        predicate = segment.substring(bracketIndex + 1, segment.length-1);
        //this.logWarn("nodeTest: " + nodeTest + ", predicate: " + predicate);
    }

    // apply nodeTest to each node
    var resultObjects = [];
    for (var i = 0; i < objects.length; i++) {
        var object = objects[i];

        // apply the node test
        if (nodeTest != isc.star) {
            object = object[nodeTest];
        } else {
            var properties = isc.getValues(object);
            object = [];
            for (var i = 0; i < properties.length; i++) {
                if (!isc.isAn.Array(properties[i])) object.add(properties[i]);
                else object.addList(properties[i]);
            }
        }

        //this.logWarn("nodeTest: " + nodeTest + ", result: " + this.echoLeaf(object));

        if (object == null) continue;

        if (!isc.isAn.Array(object)) {
            resultObjects.add(object);
        } else {
            resultObjects.addList(object);
        }
    }

    // filter result by predicate
    if (predicate) {
        // canonicalize object to an Array if we have a predicate
        var predResult = this._applyPredicateExpression(resultObjects, predicate);

        //this.logWarn("predicate expression: '" + predicate +
        //             "' applied to: " + this.echoLeaf(resultObjects) +
        //             ", with result: " + this.echoLeaf(predResult));
        resultObjects = predResult;
    }

    if (segments == null || segments.length == 0) return resultObjects;

    //this.logWarn("recursing with remaining path: " + segments.join("/"));

    // recurse if there are more segments
    path += segment + isc.slash;
    return this._selectObjects(segments, resultObjects, path);
},

_applyPredicateExpression : function (objects, expr) {
    // check for simple index (this will actually accept anything that starts with a
    // number)
    var index = parseInt(expr);
    if (!isNaN(index)) {
        // xPath indices are 1-based
        return [objects[index - 1]];
    }

    if (expr == "last()") return [objects.last()];

    // NOTE: not making property first char vs remaining chars distinction in XML "QName"
    // identifier.  Allowing () as a quick way to allow the position() function
    var parts = expr.match(/^([a-zA-Z_0-9:\-\.\(\)]*)\s*(<|>|!=|=|<=|>=|)\s*(.*)$/),
        property, operator, value;
    //this.logWarn("predicate parts: " + parts);

    if (parts == null) {
        // assume just an identifier
        if (!expr.match(/^[a-zA-Z_0-9:\-\.]*$/)) {
            this.logWarn("couldn't parse predicate expression: " + expr);
            return null;
        }
        property = expr;
    } else {
        property = parts[1], // parts[0] is the entire match
        operator = parts[2],
        value = parts[3];
    }

    // convert this simple expression to a JavaScript expression we can apply to each object


    // XPath uses single = operator
    if (operator == "=") operator = "==";

    // XPath uses functions for true and false literals
    if (value == "true()") value = true;
    else if (value == "false()") value = false;

    // support the position() function specially, by passing in params
    if (property == "position()") property = "position";

    //this.logWarn("property: " + property + ", operator: " + operator + ", value: " + value);
    var predFunc = isc._makeFunction("item,position",
        "return " +
            (property != "position" ? "item." : "") + property +
            (operator ? operator + value : ""));

    // apply the function to each object
    var matchingObjects = [];
    //this.logWarn("predicate function: " + predFunc);
    for (var i = 0; i < objects.length; i++) {
        if (predFunc(objects[i], i+1)) matchingObjects.add(objects[i]);
    }
    return matchingObjects;
},

//> @classMethod XMLTools.selectNodes()
// Retrieve a set of nodes from an XML element or document based on an XPath expression.
// <P>
// If the target document is namespaced, namespace prefixes declared in the document element of
// the target document will be available, as well as the default namespace, if declared, under
// the prefix "default".
// <P>
// To declare your own namespace prefixes, provide a prefix to URI mapping as a simple JS
// Object, for example:
// <pre>
//   {
//      az : "http://webservices.amazon.com/AWSECommerceService/2005-03-23",
//      xsd : "http://www.w3.org/2001/XMLSchema"
//   }
// </pre>
// <P>
// <b>NOTE:</b> this API cannot be supported on the Safari web browser for versions earlier
// than 3.0.3.
//
// @param element (XMLElement or XMLDocument or String)  Native XMLElement,document, or xml string
//                                                       to select from
// @param expression (XPath)   XPath expression to use to select nodes
// @param [namespaces] (prefix -> URI mapping) namespace mapping used by the expression
// @return (Array) list of nodes matching XPath
//
// @group xmlTransform
// @visibility xmlBinding
// @example xmlServerValidationErrors
//<
selectNodes : function (element, expression, namespaces, single) {
    if (isc.isA.String(element)) {
        element = this.parseXML(element);
    }

    if (isc.Browser.isSafari && (isc.Browser.isApollo || (isc.Browser.safariVersion < 522)))
    {
        this._warnIfNativeXMLUnavailable("XPath");
        return this.safariSelectNodes(element, expression, namespaces, single);
    }

    if (isc.isAn.XMLDoc(element)) {
        return element.selectNodes(expression, namespaces, single);
    }
    var start = isc.timestamp();
    var returnValue = this._selectNodes(element, expression, namespaces, single);
    var end = isc.timestamp();

    if (this.logIsInfoEnabled("xmlSelect")) {
        this.logInfo("selectNodes: expression: " + expression +
                     " returned " + this.echoLeaf(returnValue) +
                      ": " + (end-start) + "ms", "xmlSelect");
    }

    return returnValue;
},

// Do very crude emulation of XPath for older Safari and older WebKit (like Adobe AIR)
// where native XPath is not available.

safariSelectNodes : function (element, expression, namespaces, single) {

    var elements = [];

    if (!expression) {
        return null;
    }
    var recordName = expression.substring(expression.indexOf(":")+1);
    var pickUpSubElements;
    if (recordName.endsWith("/*")) {
        pickUpSubElements = true;
        recordName = recordName.substring(0, recordName.indexOf("/*"));
    }

    // NOTE: a tagName of "record" matches <foo:record> in Moz, but NOT in IE
    var nodeList = element.getElementsByTagName(recordName);

    if (pickUpSubElements && nodeList.length > 0) {
        var parent = nodeList[0];
        nodeList = parent.childNodes;
    }


    for (var i = 0; i < nodeList.length; i++) {
        // don't pick up text nodes -- can happen when iterating through childNode array
        // to simulate /* xpath
        if (nodeList[i].nodeType == 3) continue;
        elements.add(nodeList[i]);
    }
    // don't create a spurious Array for the most common case of a singular body
    // element

    if (pickUpSubElements && elements.length == 1) elements = elements[0];
    return elements;
},


// namespaces: map from prefix -> namespaceURI
// prefixes: option list of prefixes
_generateNamespaces : function (namespaces, prefixes, indent) {
    if (namespaces == null) return isc.emptyString;

    if (prefixes == null) prefixes = isc.getKeys(namespaces);

    var buffer = isc.SB.create(),
        indent = (indent != null ? "\n" + indent : "");
    for (var i = 0; i < prefixes.length; i++) {
        var prefix = prefixes[i];
        buffer.append(indent, " xmlns:", prefix, '="', namespaces[prefix], '"');
    }
    return buffer.toString();
},

// called for Moz and Safari only - return a namespace to use for the "default:" prefix in
// XPath selection
_getDefaultNamespace : function (docElement) {
    // check for a default namespace on the document element.  Note this will get
    // the default namespace established by the document element, even if the
    // document element itself is in another namespace
    var docNS = docElement.lookupNamespaceURI("");

    if (isc.Browser.isSafari && (docNS == null || docNS == "")) {
        docNS = docElement.getAttribute("xmlns");
    }
    // fall back to the namespace of the document element, even if it's not a
    // default namespace
    if (docNS == null) docNS = docElement.namespaceURI;
    // fall back to non-namespaced elements
    if (docNS == null) docNS = "";

    return docNS
},

_selectNodes : function (element, expression, namespaces, single) {


    if (element == null) return;
    var doc = element.ownerDocument;
    if (doc == null && element.documentElement) {
        // a document object was passed
        doc = element;
        element = doc.documentElement;
    }
    // in the case of an empty response, doc could still be null, so return
    if (doc == null) return null;
    if (isc.Browser.isIE) {

        if (isc.Browser.version > 5.5) {
            doc.setProperty("SelectionLanguage", "XPath");

            var nsString = this._makeIEDefaultNamespaces(doc, namespaces);
            if (namespaces) nsString += this._generateNamespaces(namespaces);
            if (this.logIsDebugEnabled("xmlSelect")) {
                this.logDebug("selectNodes: expression: " + expression +
                              ", using namespaces: " + nsString, "xmlSelect");
            }
            doc.setProperty("SelectionNamespaces", nsString);
        }

        // if "single" was passed, select a single node
        if (single) return element.selectSingleNode(expression);

        // otherwise return an Array of nodes
        var nodes = element.selectNodes(expression);

        // convert native NodeList object to a JavaScript Array
        return this._nodeListToArray(nodes);
    }


    var baseResolver = doc.createNSResolver(doc.documentElement),
        defaultNamespace = this._getDefaultNamespace(doc.documentElement);
    if (this.logIsDebugEnabled("xmlSelect")) {
        this.logDebug("Using namespaces: " + isc.echo(namespaces) +
                      ", defaultNamespace: '" + defaultNamespace + "'", "xmlSelect");
    }
    var resolver = function (prefix) {
        // supplied namespaces first
        if (namespaces && namespaces[prefix]) return namespaces[prefix];
        if (prefix == "default") return defaultNamespace;
        return baseResolver.lookupNamespaceURI(prefix);
    };
    // 0 is resultType = XPathResult.ANY_TYPE
    var results = doc.evaluate(expression, element, resolver, 0, null);

    // if "single" was passed, just return the first node
    if (single) return results.iterateNext();

    // convert native nodeList object to JavaScript Array
    return this._nodeListToArray(results);
},

// convert a native NodeList object to a JavaScript Array.
// NodeLists are returned by native selectNodes() in IE or document.evaluate(xpath) in other
// browsers.  xmlElement.childNodes is also a NodeList
_nodeListToArray : function (nodeList) {
    var output = [];


    if (isc.Browser.isIE || nodeList.iterateNext == null) {
        for (var i = 0; i < nodeList.length; i++) {
            output.add(nodeList.item(i));

        }
    } else {
        var resultNode;
        while (resultNode = nodeList.iterateNext()) {
            output.add(resultNode);
        }
    }
    return output;
},

getElementChildren : function (element) {
    var output = [],
        childNodes = element.childNodes;
    for (var i = 0; i < childNodes.length; i++) {
        var child = childNodes[i];
        if (this.isTextNode(child)) continue;
        output.add(child);
    }
    return output;
},

//> @classMethod XMLTools.selectString() [A]
// Retrieve a string value from an XML element or document based on an XPath expression.
// <P>
// If more than one node matches, only the first node's value will be returned.
// <P>
// Namespacing works as described under +link{XMLTools.selectNodes()}
// <P>
// <b>NOTE:</b> this API cannot be supported on the Safari web browser for versions prior to
// 3.0.3.
//
// @param element (XMLElement or XMLDocument or String)  Native XMLElement,document, or xml string
//                                                       to select from
// @param expression (XPath)   XPath expression to use to select nodes
// @param [namespaces] (prefix -> URI mapping) namespace mapping used by the expression
//
// @return (String) result of the XPath, in String form
//
// @group xmlTransform
// @visibility xmlBinding
// @example xmlServerValidationErrors
//<
selectString : function (element, expression, namespaces) {
    return this.selectScalar(element, expression, namespaces);
},

//> @classMethod XMLTools.selectNumber() [A]
// Retrieve a numeric value from an XML element or document based on an XPath expression.
// <P>
// If more than one node matches, only the first node's value will be returned.
// <P>
// Namespacing works as described under +link{XMLTools.selectNodes()}
// <P>
// <b>NOTE:</b> this API cannot be supported on the Safari web browser for versions prior to
// 3.0.3.
//
// @param element (XMLElement or XMLDocument or String)  Native XMLElement,document, or xml string
//                                                       to select from
// @param expression (XPath)   XPath expression to use to select nodes
// @param [namespaces] (prefix -> URI mapping) namespace mapping used by the expression
//
// @return (Number) result of the XPath, in Number form
//
// @group xmlTransform
// @visibility xmlBinding
//<
selectNumber : function (element, expression, namespaces) {
    return this.selectScalar(element, expression, namespaces, true);
},

selectScalar : function (element, expression, namespaces, asNumber) {
    if (isc.isA.String(element)) element = this.parseXML(element);
    if (isc.isAn.XMLDoc(element)) return element.selectScalar(expression, namespaces, asNumber);

    // NOTE: the XPath standard allows you to ask for a specific "resultType", eg String or
    // Number, instead of a NodeSet.  Moz does implement this.  However, for both
    // resultType:String and resultType:Number, Moz returns identical results for absence of an
    // element vs empty element, whereas we'd like to be able to return null vs "" for these
    // two cases respectively.
    //
    // In IE we can get a singular node from selectSingleNode(), and we grab the and parseInt.
    //
    // NOTE also: in Moz at least, if we don't ask for a scalar type, Moz will never return it
    // for XPathResult.ANY: single text node results and single attribute results still return
    // unordered_node_iterator by default, with crashing stringValue and numberValue accessors

    // pass "true" to ask for a single node


    var value;
    if (isc.Browser.isSafari && isc.Browser.isApollo || (isc.Browser.safariVersion < 522)) {
        var name=expression.substring(expression.indexOf(":")+1);
        value = element.getElementsByTagName(name)[0];
    } else {
        value = this.selectNodes(element,expression,namespaces,true);
    }

    if (value == null) return null;
    var text = this.getElementText(value);
    return asNumber ? parseInt(text) : text;
},

// performs xpath select and returns array of strings that are the values of selected nodes.
//
// expects result of xpath select to be a list of Attribute or Text nodes.  Performs an in-place
// replacement on the list with the node values and returns that.
// XXX internal until this handles simple elements in addition to attributes
selectScalarList : function (element, expression, namespaces) {
    if (isc.isA.String(element)) element = this.parseXML(element);
    if (isc.isAn.XMLDoc(element)) return element.selectScalarList(expression, namespaces);

    var values = this.selectNodes(element, expression, namespaces);
    // Thank god attributes and text elements share the 'nodeValue' attribute that has the data.
    for (var i = 0; i < values.length; i++) {
        values[i] = values[i].nodeValue;
    }
    return values;
},

// XSLT
// ---------------------------------------------------------------------------------------

//> @classMethod XMLTools.transformNodes()
// Apply an XSLT Stylesheet to an XML Document.
// <P>
// This method cannot currently be supported on the Safari web browser versions prior to
// 3.0.3.
//
// @param inputDocument (XMLDocument) XML document to apply the transform to
// @param styleSheet    (XMLDocument) XSLT stylesheet to use for transform
// @return (String) stylesheet output
//
// @group xmlTransform
// @visibility xmlBinding
//<
transformNodes : function (inputDocument, styleSheet) {
    if (isc.isAn.XMLDoc(inputDocument)) inputDocument = inputDocument.nativeDoc;
    if (isc.isAn.XMLDoc(styleSheet)) styleSheet = styleSheet.nativeDoc;
    if (isc.Browser.isIE) {
        if (inputDocument) {
            return inputDocument.transformNode(styleSheet);
        }
        return null;
    }


    var processor = new XSLTProcessor();
    processor.importStylesheet(styleSheet);


    if (isc.Browser.isMoz && this.mozAnchorBug && isc.Browser.geckoVersion < 20051107) {
        var ownerDocument = document.implementation.createDocument("", "test", null);
        var newFragment = processor.transformToFragment(inputDocument, ownerDocument);
        return new XMLSerializer().serializeToString(newFragment);
    }

    if (inputDocument) {
        var outputDocument = processor.transformToDocument(inputDocument);
        return new XMLSerializer().serializeToString(outputDocument);
    }

    return null;

    // transformToFragment can produce something that is not a well-formed document, for
    // example, just a text node.  However this doesn't really mean that Mozilla supports XSLT
    // to text, since any tags within the fragment become actual elements.  XMLSerializer can
    // be used to convert to String, but for some reason in also converts tags to eg "&lt;"
    //var fragment = processor.transformToFragment(inputDocument, inputDocument);
    //return new XMLSerializer().serializeToString(fragment);
},

// XML Serialize
// ---------------------------------------------------------------------------------------
//> @classMethod XMLTools.serializeToString()
// Takes an XMLDocument and returns it as a String.
// <P>
// This method is not supported on the Safari web browser versions prior to 3.0.3.
//
// @param inputDocument (XMLDocument) XML document to apply the transform to
// @return (String) XML document as a String
//
// @group xmlTransform
// @visibility xmlBinding
//<
serializeToString : function (inputDocument) {
    this._serializerXSL = this._serializerXSL || isc.xml.parseXML(
'<xsl:stylesheet version=\'1.0\' xmlns:xsl=\'http://www.w3.org/1999/XSL/Transform\'>\r' +
'<xsl:output method="xml" indent="yes"/>\r' +
'<xsl:strip-space elements="*"/>\r' +
'<xsl:template match="/">\r' +
'  <xsl:copy-of select="."/>\r' +
'</xsl:template>\r' +
'</xsl:stylesheet>');

    return this.transformNodes(inputDocument, this._serializerXSL);
},

// Schema Translation
// ---------------------------------------------------------------------------------------

//> @classMethod XMLTools.loadXMLSchema()       [A]
// Load an XML file containing XML schema definitions and create DataSource and SimpleType
// objects to represent the schema.  You can use to loaded schema to bind ISC components,
// perform validation, create editing interfaces, and build other metadata-driven interfaces.
// You can also use +link{dataSource.inheritsFrom,schema inheritance} to overlay
// presentation-specific data (such as user-visible titles) on top of XML Schema.
// <p>
// In the loaded XML Schema, all &lt;xsd:complexType&gt; declarations become SmartClient
// DataSources, and all &lt;xsd:simpleType&gt; definitions become SmartClient
// +link{SimpleType,atomic type definitions}.
// <p>
// By default, named complexType definitions and named element definitions containing
// complexTypes become global DataSources, that is, they can be fetched with
// +link{classMethod:DataSource.getDataSource()}.  Inline complexType definitions get
// automatically generated names.
// <p>
// Named simpleType declarations become global +link{SimpleType,atomic types}, that is,
// subsequently defined DataSources can use them for +link{attr:DataSourceField.type}.  XML
// schema "restrictions" for simple types are automatically translated to
// +link{attr:DataSourceField.valueMap} or +link{attr:DataSourceField.validators} as
// appropriate.
// <P>
// The created SchemaSet object is available in the callback as the single parameter
// "schemaSet", or can retrieved via <code>SchemaSet.get(schemaNamespace)</code>.
// <P>
// NOTE: unless you are building an application that dynamically loads XML Schema
// without prior knowledge, instead of calling loadXMLSchema(), you should either:
// <ul>
// <li> use the +link{group:loadXMLSchemaTag} tag to eliminate the need for an asynchronous
// download of an XML Schema file as part of application startup, <b>OR</b>
// <li> use the "WSDL" tab in the Developer Console to obtain the XML Schema definition as a
// JavaScript file that can be loaded via a normal HTML &lt;SCRIPT SRC=&gt; tag and/or
// combined with other JavaScript files.
// </ul>
// <P>
// NOTE: required fields: the XML Schema concept of "required" for an attribute or subelement,
// expressed via use="required" (for an attribute) or minOccurs > 0 (for a subelement), is
// that the attribute or element must be present in the XML document <i>but can have any
// value</i>, including being empty or null.  The SmartClient notion of required means
// non-null.  You can express the SmartClient notion of required in XML Schema with the
// combination of minOccurs>0 and a minLength or length "restriction", and SmartClient
// will recognize the field as SmartClient-required, with all of the behaviors that implies
// (eg, specially styled form titles, automatic validation, etc).
//
// @param schemaURL (URL) URL to load the schema from
// @param callback  (Callback) signature is callback(schemaSet)
// @param [requestProperties] (RPCRequest) additional properties to set on the RPCRequest
//                                         that will be issued
// @param autoLoadImports (boolean) if set, xsd:import statements will be processed
//                                  automatically to load dependent XSD files where a
//                                  "location" is specified.  The callback will not fire until
//                                  all dependencies have been loaded

//
// @group xmlSchema
// @visibility xmlBinding
// @example xmlSchemaImport
//<
loadXMLSchema : function (xmlSchemaURL, callback, requestProperties, autoLoadImports, wsProperties) {
    requestProperties = requestProperties || {};
    requestProperties.operationType = requestProperties.operationType || "loadXMLSchema";

    this.loadWSDL(xmlSchemaURL, callback, requestProperties, autoLoadImports, wsProperties, true);
},

//> @classMethod XMLTools.loadWSDL()      [A]
// Load a WSDL file and create an instance of WebService that allows invoking operations and
// binding DataSources to web service operations.
// <P>
// The created WebService object is available in the callback as the single parameter
// "service", or can be retrieved via <code>WebService.get(serviceNamespace)</code>.
// <P>
// XML Schema present in the WSDL file will also be processed as described in
// +link{XMLTools.loadXMLSchema()}.  However note that <b>imported</b> XML Schema
// (&lt;xs:import&gt; tag) will not be automatically loaded and must be loaded manually using
// +link{loadXMLSchema()} before the loaded service will be usable.  This is because the WSDL
// spec allows but does not require a valid URL to be provided for loading imported XML Schema.
// <P>
// NOTE: unless you are building an application that dynamically contacts WSDL web services
// without prior knowledge, instead of calling loadWSDL(), you should either:
// <ul>
// <li> use the +link{group:loadWSDLTag} tag to eliminate the need for an asynchronous
// download of a WSDL file as part of application startup, <b>OR</b>
// <li> use the "WSDL" tab in the Developer Console to obtain the WebService definition as a
// JavaScript file that can be loaded via a normal HTML &lt;SCRIPT SRC=&gt; tag and/or combined
// with other JavaScript files.
// </ul>
// Platform notes:
// <ul>
// <li> loadWSDL() is not supported in Safari 2.0 (but is supported in Safari 3.0.3 and greater)
// However, you can use either approach mentioned above (loadWSDLTag or JavaScript file) with
// Safari pre 3.0.3.
// <li> if you are using a non-Java server, in order to obtain a JavaScript file representing a
// web service, you must run the Developer Console in the Java-based SmartClient SDK
// </ul>
//
// @param wsdlURL  (URL) URL to load the WSDL file from
// @param callback (Callback) signature is callback(service)
// @param [requestProperties] (RPCRequest) additional properties to set on the RPCRequest
//                                         that will be issued
// @param autoLoadImports (boolean) if set, xsd:import statements will be processed
//                                  automatically to load dependent XSD files where a
//                                  "location" is specified.  The callback will not fire until
//                                  all dependencies have been loaded
//
// @group xmlSchema
// @visibility xmlBinding
// @example WSDLDataSource
//<
// NOTE: returnSchemaSet is an internal parameter to allow the loadWSDL logic to also be used
// by loadXMLSchema()
loadWSDL : function (xmlSchemaURL, callback, requestProperties, autoLoadImports, wsProperties, returnSchemaSet) {
    // load the schema translator if it hasn't been loaded, not doing translation until it's
    // done loading
    if (!this._schemaTranslator) {
        var url = isc.Page.getIsomorphicClientDir() + "schema/schemaTranslator.xsl";
        // get rid of the http://, or loadXML will assume it has to use the HTTPProxy to load
        // the file
        url = url.replace(/https?:\/\/[^\/]*\//, "/");
        this._schemaTranslator = "LOADING"; // prevent multiple attempts to load translator
        isc.xml.loadXML(url, function (xmlDoc, xmlText, rpcResponse) {
            isc.xml.logDebug("schema translator loaded");

            if (isc.Browser.isMoz && rpcResponse.xmlHttpRequest &&
                rpcResponse.xmlHttpRequest.responseXML)
            {
                isc.xml._schemaTranslator =
                        isc.XMLDoc.create(rpcResponse.xmlHttpRequest.responseXML);
            } else {
                isc.xml._schemaTranslator = xmlDoc;
            }
            isc.xml.loadWSDL(xmlSchemaURL, callback, requestProperties,
                             autoLoadImports, wsProperties, returnSchemaSet);
        });
        return;
    }

    requestProperties = requestProperties || {};
    requestProperties.operationType = requestProperties.operationType || "loadWSDL";

    var context = {
        location: xmlSchemaURL,
        callback : callback,
        autoLoadImports: autoLoadImports,
        wsProperties: wsProperties || {},
        returnSchemaSet: returnSchemaSet
    };

    isc.xml.loadXML(xmlSchemaURL, function (xmlDoc,xmlText,rpcResponse,rpcRequest) {
                        context.rpcResponse = rpcResponse;
                        context.rpcRequest = rpcRequest;
                        isc.xml._loadSchemaReply(xmlDoc, context);
                    },
                    requestProperties);
},

// load a WSDL service from XML text, an XMLDoc or XML elements that are already loaded
loadWSDLFromXML : function (xmlDoc, callback, autoLoadImports, wsProperties, returnSchemaSet) {
    if (isc.isA.String(xmlDoc)) xmlDoc = isc.xml.parseXML(xmlDoc);

    this._loadSchemaReply(xmlDoc, { callback:callback, autoLoadImports:autoLoadImports,
                                    wsProperties:wsProperties, returnSchemaSet:returnSchemaSet });
},

// whether to use client-side XML2JS to translate WSDL/XMLSchema definitions to live objects
useClientXML : true,
_loadSchemaReply : function (xmlDoc, context) {
    // NOTE: check that translator is an XML doc, not null, since we use a loading marker
    if (!isc.isAn.XMLDoc(this._schemaTranslator)) {
        this.logInfo("deferred schema translator, schema translator not loaded", "xmlComm");
        isc.Timer.setTimeout({ methodName: "_loadSchemaReply", target:this,
                               args:[xmlDoc, context] });
        return;
    }
    this.logInfo("transforming schema: " + this.echoLeaf(xmlDoc) +
                 " with translator " + this.echoLeaf(this._schemaTranslator), "xmlComm");
    var xmlText = this.transformNodes(xmlDoc, this._schemaTranslator);

    if (this.logIsDebugEnabled("xmlComm")) {
        this.logWarn("XML service definition is: \n" + xmlText);
    }

    var wsProperties = context.wsProperties,
        initiator = wsProperties.initiator;
    if (wsProperties.captureXML) {
        wsProperties.xmlSource = xmlText;
        if (initiator) initiator.addImportXMLSource(xmlText, context.location);
    }

    if (this.useClientXML) {
        var xmlDoc = isc.xml.parseXML(xmlText),
            elements = xmlDoc ? this._nodeListToArray(xmlDoc.documentElement.childNodes) : null,
            jsResult = elements ? this.toJS(elements, null, null, true) : null
        ;

        //this.logWarn("XML service definition is: \n" + xmlText);

        //this.logWarn("js result: \n" + this.echoFull(this.toJS(elements)));

        this._loadSchemaToJSReply(context);
        return;
    }

    this.logInfo("about to call serverToJS with: " + this.echoLeaf(xmlText) +
                 ", callback: " + this.echo(context.callback), "xmlComm");
    this.serverToJS(xmlText, function () {
                        isc.Log.logWarn("serverToJS returned");
                        isc.xml._loadSchemaToJSReply(context);
                    });
},
_loadSchemaToJSReply : function (context) {

    var loadedObject;
    if (context.returnSchemaSet) {
        loadedObject = isc.SchemaSet._lastLoaded;
    } else {
        // fallback covers us in case someone loads a WSDL that really don't have a workable
        // service definition but contains schema
        loadedObject = isc.WebService._lastLoaded || isc.SchemaSet._lastLoaded;
    }
    isc.WebService._lastLoaded = isc.SchemaSet._lastLoaded = null;

    // if we didn't load an object and willHandleError is false, return without firing the
    // callback
    if (!loadedObject && context.rpcRequest.willHandleError == false) return;

    // tell the service/schemaSet the location it was loaded from - needed for knowing the
    // correct relative path to load imported schema
    if (loadedObject) loadedObject.location = context.location;

    if (context.wsProperties && loadedObject) loadedObject.setProperties(context.wsProperties);

    // tack on the RPCRequest - allows context to be kept across a loadWSDL call
    var argNames = (isc.isA.WebService(loadedObject) ? "service" : "schemaSet") + ",rpcRequest";
    var args = [loadedObject,context.rpcRequest];

    // if autoLoadImports was passed, tell the service/schemaSet to load imported schema
    if (context.autoLoadImports && loadedObject && loadedObject.loadImports) {
        var _this = this;
        loadedObject.loadImports(function () {
            _this._completeLoad(context.callback, argNames, args);
        });
    } else {
        this._completeLoad(context.callback, argNames, args);
    }
},

_completeLoad : function (callback, argNames, args) {
    //this.logWarn("firing callback: " + this.echo(callback) +
    //             " with argName: " + argName +
    //             " value: " + arg);

    this.fireCallback(callback, argNames, args);
},

getCompleteSource : function (service, callback, asXML) {
    var importSources = service.importSources;
    if (!importSources) return "";

    importSources = importSources.getProperty("xmlText");
    // include the source of the webService or schemaSet that imported all of these
    // dependencies
    importSources.unshift(service.xmlSource);
    // remove <?xml directives
    importSources = this.map("trimXMLStart", importSources);
    var source = importSources.join("\n");

    //this.logWarn("sources: " + this.echo(importSources));

    if (asXML) {
        this.fireCallback(callback, "source", [source]);
        return;
    }

    this.toJSCode(source, function (rpcResponse, data) {
        this.fireCallback(callback, "source", [data]);
    })
}

// SmartClient Component XML
// ---------------------------------------------------------------------------------------

//> @groupDef componentXML
// <smartclient>
// As covered in the <i>QuickStart Guide</i> Chapter 4, <i>Coding</i>, SmartClient
// components can be created in either XML or JavaScript format.  This section covers further
// details of using the XML format, called "SmartClient component XML".
// <P>
// </smartclient>
// <smartgwt>
// Component XML is an XML format for declaring Smart GWT components and screen definitions.
// Available with Smart GWT Pro and above, Component XML is the same format used by Visual
// Builder to save screens.
// <P>
// By allowing you to keep layout information and property settings in an XML format, Component
// XML enables non-developers to build and maintain portions of your application, either by
// editing screens within Visual Builder or by directly editing the XML itself.
// <P>
// Unlike the similar GWT "UIBinder" technology, Component XML does not require a compilation
// step.  XML screen definitions can be generated on the fly, modified at runtime, stored in a
// database, and in all other ways treated as a dynamic resource.  See the section "Dynamic
// Component XML" for details.
// <P>
// <h3>Basic Usage</h3>
// <P>
// To create a Smart GWT component in XML code, you create a tag with the component's class
// name. You can set that component's properties either as tag attributes:
// <pre>
//   &lt;Button title="Click me" width="200" /&gt;
// </pre>
// or in nested tags:
// <pre>
//   &lt;Button&gt;
//     &lt;title&gt;Click me&lt;/title&gt;
//     &lt;width&gt;200&lt;/width&gt;
//   &lt;/Button&gt;
// </pre>
// <P>
// To set a property that is an Array of simple types (like int, or String), repeat tags like
// so (for +link{dynamicForm.colWidths}):
// <P>
// <pre>
// &lt;DynamicForm&gt;
//     &lt;numCols&gt;2&lt;/numCols&gt;
//     &lt;colWidths&gt;250&lt;/colWidths&gt;
//     &lt;colWidths&gt;*&lt;/colWidths&gt;
// &lt;/DynamicForm&gt;
// </pre>
// To set a property that takes an Array of complex objects, use the property name as a
// container tag, then create further nested tags for the objects in the array, like so (for
// +link{listGrid.fields}):
// <P>
// <pre>
// &lt;ListGrid&gt;
//     &lt;fields&gt;
//         &lt;ListGridField name="accountName" ... /&gt;
//         &lt;ListGridField name="accountType" ... /&gt;
//     &lt;/fields&gt;
// &lt;/ListGrid&gt;
// </pre>
// <P>
// This same approach works for creating nested layouts, such as placing a ListGrid in a
// VLayout:
// <P>
// <pre>
// &lt;VLayout&gt;
//     &lt;members&gt;
//         &lt;ListGrid .. /&gt;
//     &lt;/members&gt;
// &lt;VLayout&gt;
// </pre>
// <P>
// </smartgwt>
// <b>Referring to previously defined components</b>
// <P>
// To refer to another component by ID in XML, use &lt;Canvas ref=/&gt;.  For example:
// <pre>
// &lt;Canvas ID="myCanvas"/&gt;
// &lt;Canvas ID="myCanvas2"/&gt;
// &lt;VLayout&gt;
//     &lt;members&gt;
//         &lt;Canvas ref="myCanvas"/&gt;
//         &lt;Canvas ref="myCanvas2"/&gt;
//     &lt;/members&gt;
// &lt;/VLayout&gt;
// </pre>
// <P>
// <h3>Loading screens stored in Component XML</h3>
// <P>
// Save your Component XML as a file called <i>screenName</i>.ui.xml under
// <i>webroot</i>/shared/ui/.  Placing your .ui.xml file in this directory makes it visible to
// the system; the location of this directory can be configured in +link{server_properties,server.properties}
// by setting
// the <i>project.ui</i> property.  <i>screenName</i> can be any valid identifier (no spaces,
// dashes or periods - underscores OK).
// <P>
// If you have multiple top-level tags (eg, your code is similar to the example above under
// "Referring to previousy defined components") use &lt;isomorphicXML&gt; as a top-level
// container tag - this has no impact on processing and is just an idiom to make your file valid
// XML, since XML does not allow multiple top-level tags in a document.
// <P>
// Component XML screens are then loaded using the ScreenLoaderServlet.  The default SDK comes
// with this servlet already registered at
// <smartclient><i>projectBase</i>/isomorphic/screenLoader</smartclient>
// <smartgwt><i>projectBase</i>/sc/screenLoader</smartgwt>.  If you've modified web.xml
// or only included some of the default servlets, you may need to add it now - see the
// <smartclient>+link{group:iscInstall,Installation Instructions}</smartclient>
// <smartgwt>+link{group:sgwtEESetup,Installation Instructions}</smartgwt>.
// <P>
// To create an application that consists of <i>just</i> the imported mockup, just add a
// &lt;script src&gt; tag pointing to the ScreenLoader servlet and referring to the
// <i>screenName</i> you used when you saved your file.
// <smartclient>
// For example, if your application launches from a directory next to the "isomorphic"
// directory from the SDK:
// <pre>
//    &lt;script src="../isomorphic/screenLoader?screenName=<i>screenName</i>"&gt;&lt;/script&gt;
// </pre>
// </smartclient>
// <smartgwt>
// For example, add the following to your bootstrap .html file:
// <pre>
//    &lt;script src="sc/screenLoader?screenName=<i>screenName</i>"&gt;&lt;/script&gt;
// </pre>
// </smartgwt>
// If you want to load screens dynamically, or if you want to load more than one screen, use
// +link{RPCManager.loadScreen()}.  See the section on "Multiple screens and global IDs"
// below.
// <P>
// <smartclient>
// <h3>Embedding JavaScript code</h3>
// <P>
// To embed a JavaScript expression into component XML, use the &lt;JS&gt; tag.  For example:
// <pre>
// &lt;VLayout&gt;
//     &lt;width&gt;&lt;JS&gt;isc.Page.getWidth() - 20&lt;/JS&gt;&lt;/width&gt;
// &lt;/VLayout&gt;
// </pre>
// Note that, like all component XML properties, the <code>width</code> property can be
// specified either as an XML attribute or as a subelement.  Expressing it as a subelement, as
// shown above, allows the &lt;JS&gt; tag to be used.
// <P>
// <b>Embedding Methods</b>
// <P>
// For +link{group:stringMethods,StringMethods} such as +link{listGrid.recordClick()},
// JavaScript code can be used as an ordinary element value:
// <pre>
// &lt;ListGrid&gt;
//     &lt;recordClick&gt;if (record.age &gt; 65) doSomething()&lt;/recordClick&gt;
// &lt;/ListGrid&gt;
// </pre>
// To embed an actual function definition, use the &lt;JS&gt; tag described above.  For
// example:
// <pre>
// &lt;ListGrid&gt;
//     &lt;recordClick&gt;&lt;JS&gt;function (viewer, record, recordNum, field) {
//          if (record.age &gt; 65) doSomething();
//     }&lt;/JS&gt;&lt;/recordClick&gt;
// &lt;/ListGrid&gt;
// </pre>
// Unfortunately, characters commonly used in JavaScript code, such as ampersand (&amp;), are
// not legal inside XML element or attribute values.  For example, the expression "record !=
// null && record.age &gt; 65" must be written as shown below, or it is not considered valid XML:
// <P>
// <pre>
// &lt;ListGrid&gt;
//     &lt;recordClick&gt;
//         if (record.status != null &amp;amp;&amp;amp; record.age &gt; 65) doSomething()
//     &lt;/recordClick&gt;
// &lt;/ListGrid&gt;
// </pre>
// An alternative, for larger blocks of code, is to use the XML standard "CDATA" (character
// data) processing directive, which allows ampersand and other characters to be used without
// special notation:
// <pre>
// &lt;ListGrid&gt;
//     &lt;recordClick&gt;&lt;![CDATA[
//         if (record.status != null && record.age &gt; 65) doSomething()
//     ]]&gt;&lt;/recordClick&gt;
// &lt;/ListGrid&gt;
// </pre>
// <P>
// Overall, embedding code in XML can be awkward.  Isomorphic generally recommends that
// significant chunks of JavaScript code, such as non-trivial custom components, be moved to
// separate, purely JavaScript files, while code embedded in component XML is limited to simple
// expressions and short functions.  See the next section for details on keeping code in a
// separate file.
// </smartclient>
// <P>
// <h3>Event Handlers &amp; Scripting loaded components</h3>
// <P>
// You can retrieve the components in your loaded screen in order to add event handlers to
// them, call APIs on them, place them into layouts you programmatically create, and in general
// add dynamic behavior.  Retrieve the components via the +link{Canvas.getById()} API
// (note, when working with multiple screens, be sure to see the upcoming section about managing
// global IDs).
// <P>
// You can then add event handlers normally.  For example, say there is a ListGrid with ID
// "mainGrid" and a DynamicForm with ID "editForm" in the same screen, and you want to populate
// the form with whatever record is clicked on in the grid:
// <P>
// <smartclient>
// <pre>
//   isc.Canvas.getByID("mainGrid").addMethods({
//       recordClick : "editForm.editRecord(record)"
//   });
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
//   ListGrid grid = (ListGrid)Canvas.getByID("mainGrid");
//   final DynamicForm form = (DynamicForm)Canvas.getByID("editForm");
//   grid.addRecordClickHandler(new RecordClickHandler() {
//       public void onRecordClick(RecordClickEvent event) {
//           form.editRecord(event.getRecord());
//       }
//   });
// </pre>
// </smartgwt>
// <P>
// You can also add a loaded screen to an existing layout container.  For example, perhaps you've
// already written parts of the application via normal coding techniques, and now you want to take
// a screen defined in Component XML and place it in a particular Layout you've already created
// ("existingLayout" below) - just use +link{layout.addMember()} as usual:
// <smartgwt>
// <pre>
//    existingLayout.addMember(Canvas.getById("<i>componentId</i>"));
// </pre>
// </smartgwt>
// <smartclient>
// <pre>
//    existingLayout.addMember(isc.Canvas.getById("<i>componentId</i>"));
// </pre>
// </smartclient>
// Component XML files can also refer to components you have created programmatically, and incorporate
// them into layouts.  For example, if you have created a ListGrid component with ID "theGrid", you could
// refer to that grid using a <code>&lt;Canvas ref=""/&gt;</code> tag, which can be used anywhere a
// Canvas is expected.  For example:
// <pre>
// &lt;VLayout ... &gt;
//     &lt;members&gt;
//           &lt;Canvas ref="theGrid"/&gt;
//     &lt;/members&gt;
// &lt;/VLayout&gt;
// </pre>
// Note that this approach requires that the referenced component has been created <b>before</b>
// <code>loadScreen</code> is called.
// <P>
// <h3>Component XML and global IDs</h3>
// <P>
// A Component XML screen created in Visual Builder or via the
// +link{group:balsamiqImport,Balsamiq importer} will assign global IDs to all components
// generated from your mockup so that you can retrieve them by ID to add event handlers and
// call APIs.   However if you build an application out of multiple screens built at different
// times, these IDs can collide, which will cause components to overwrite each other as they
// each try to use the same ID.
// <P>
// To solve this, the +link{RPCManager.loadScreen()} API will <i>ignore</i> global IDs on loaded
// components, assigning them sequential generated IDs instead (which will never collide).
// Instead of using global IDs, the callback for <i>loadScreen()</i> will automatically provide
// you with the outermost component of a loaded screen, and that outermost component will
// provide access to other components by their original IDs via +link{canvas.getByLocalId()}.
// <P>
// This allows you to add loaded screens to existing layouts, attach event handlers and take
// other programmatic actions, all without ever establishing global IDs.
// <P>
// <h3>Loading multiple screens</h3>
// <P>
// A typical application that uses screens stored in Component XML will have several such
// screens, or in some cases, hundreds or thousands.  +link{RPCManager.cacheScreens()} can be
// used to load a set of screen definitions without actually creating any UI components - a
// subsequent call to +link{RPCManager.createScreen()} is used to actually create the screen
// when needed.  These two APIs provide the same global ID management facilities as
// +link{RPCManager.loadScreen(),loadScreen()}.
// <P>
// As discussed in the +link{group:smartArchitecture,SmartClient Architecture overview}, screen
// definitions are typically very small, and should be loaded entirely up front or in very
// large batches.  Further, screen <i>definitions</i> have essentially negligible runtime
// overhead until the screen is actually created.
// <P>
// Therefore, use the following best practices for screen loading, even if you have very few or
// only one screen defined in Component XML:
// <ul>
// <li> at application startup, load all screens using +link{RPCManager.cacheScreens()}
// <li> create screens lazily (when they are about to be shown to the end user) using
//      +link{RPCManager.createScreen()}.
// <li> for applications with very very large numbers of screens where loading all screen
//      definitions up front creates a very large download, consider multiple calls to
//      <code>cacheScreens()</code>, loading sets of screens that are likely to be used
//      together.
// </ul>
// <P>
// <h3>Dynamic Component XML</h3>
// <P>
// Components can be dynamically provided on the server side by using the API defined in
// {@link com.isomorphic.servlet.ScreenLoaderServlet#addDynamicScreenGenerator(DynamicScreenGenerator)}
// which allows adding DynamicScreenGenerators to system for providing the .ui.xml files on the fly:
// <pre>
// ScreenLoaderServlet.addDynamicScreenGenerator(new DynamicScreenGenerator() {
//      public String getScreen(String id) {
//
//        if (id.equals("testDynamicScreenPrefix3")) {
//            return null;
//        }
//
//        id=id.replace("testDynamicScreen","");
//        return "<VLayout ID=\""+id+"\" border=\"1px solid blue\"/>";
//      }
//  }, "testDynamicScreenPrefix");
// </pre>
// <P>
// Whenever the system needs a screen in future, it will first call the registered DynamicScreenGenerator's
// <code>getScreen(String)</code> method for providing the given screen; Only if all queried
// DynamicScreenGenerators returns <code>null</code>, will proceed to use the normal system for obtaining
// the screen instances.
// <P>
// NOTE:<ul>
// <li>If this API is used, DynamicScreenGenerator will be called for <b>every</b> screen that
// the framework needs. Instead of this, the API contains alternative methods which will allow
// adding DynamicScreenGenerators only for a given string prefix or a regular expression.
// </ul>
// <P>
// In the provided example we register a DynamicScreenGenerator which will be called for each screen
// the system tries to load, if it starts with "testDynamicScreenPrefix", except the screen with id
// "testDynamicScreenPrefix3" for which we return null.
// <P>
// While registering a DynamicScreenGenerator is the first choice since is compatible
// with ScreenLoaderServlet's ability to load several screens in a single HTTPRequest,
// there are two additional ways to load Component XML screens - you can create a .jsp that
// uses the JSP tags that come with the SDK:
// <pre>
//    &lt;%@ taglib uri="/WEB-INF/iscTaglib.xml" prefix="isomorphic" %&gt;
//    &lt;isomorphic:XML&gt;
//       ... Component XML ...
//    &lt;/isomorphic:XML&gt;
// </pre>
// <P>
// Or you can use the server-side API com.isomorphic.XML.toJS():
// <pre>
//     XML.toJS("&lt;isomorphicXML xmlns:xsi=\"nativeType\"&gt;" +
//                  componentXMLCode +
//                  "&lt;/isomorphicXML&gt;");
// </pre>
// However these two approaches will allow to only load one screen at a time.
// The JSP code above and the programmatic call to XML.toJS() both return a JavaScript code,
// which is the response that +link{RPCManager.loadScreen()} expects.  The
// <code>XML.toJS()</code> API can be easily combined with
// +link{group:standaloneDataSourceUsage,direct use of the server-side DataSource API} to build
// a version of the ScreenLoaderServlet that can retrieve Component XML from a database
// or any Java API.
// <P>
// For static Component XML screens (cannot be changed at runtime), you can optionally run the
// XML.toJS() process as a build step to save a small amount of runtime overhead in XML to JS
// translation.  Use +link{RPCManager.loadScreen()} to load the resulting JavaScript by
// overriding the +link{rpcRequest.actionURL} to point to the generated JavaScript file.
// Note that the overhead is minor enough that this is not worth doing unless you have a very
// large deployment and a very large number of static Component XML files.
// <P>
// <h3>Troubleshooting</h3>
// <P>
// XML parsing errors, which happen when XML is not well-formed and would be rejected by any
// standard XML parser, are reported in the server-side log, and, when possible, also in the
// client-side log within the "Results" tab of the Developer Console.
// <P>
// If you are loading a screen via the +link{RPCManager.loadScreen()} API, you can see the
// response from the server in the RPC tab of the Developer Console - this will show you issues
// such as a misplaced ScreenLoaderServlet (HTTP response code will be 404 - Not Found) or
// responses that contain server exception details instead of the expected JavaScript response.
// <P>
// <smartclient>
// Other issues with component XML can result from incorrect use of SmartClient component XML
// tags.  For example, you may specify a property and it may appear to have no effect even
// though it clearly works in other, JavaScript-based examples.  If you get this symptom, you
// can troubleshoot by looking at the JavaScript code SmartClient generates from component XML.
// <P>
// </smartclient>
// <P>
// You can also use the "Eval XML" section in the "Results" tab of the Developer Console to
// interactively experiment with Component XML ("Eval XML" button) and as a means of seeing the
// generated JavaScript ("Show JS" button).
// <P>
// <h3>Localization / Internationalization</h3>
// <P>
// Component XML files support embedding references to messages loaded from ResourceBundles via
// the same JSTL-like &lt;fmt&gt; syntax as is used for DataSource .ds.xml files.  See
// +link{group:dataSourceLocalization,DataSource localization for details}.
// <P>
// <smartclient>
// <h3>Custom Properties</h3>
// <P>
// If you specify a custom property on a component in XML, for example:
// <pre>
// &lt;Canvas myProperty="false"/&gt;
// </pre>
// The value of the property will be a JavaScript String.  In the above example, it would be
// the string "false", which is considered a boolean true value in the JavaScript language.
// If you want a different JavaScript type, you can force a property to be interpreted as a
// given type by using the "xsi:type" attribute:
// <pre>
// &lt;Canvas&gt;
//     &lt;myProperty xsi:type="xsd:boolean"&gt;false&lt;/myProperty&gt;
// &lt;/Canvas&gt;
// </pre>
// The same notation works when you want to declare that an entire subobject has a given type.
// For example, this would cause the custom property "myListGrid" to have a live
// +link{ListGrid} instance as it's value.  All of the properties on the &lt;myListGrid&gt; tag
// will be correctly interpreted as ListGrid properties and have the correct types.
// <pre>
// &lt;Canvas&gt;
//     &lt;myListGrid xsi:type="ListGrid" width="500" height="600"/&gt;
// &lt;/Canvas&gt;
// </pre>
// If you do not want an actual live ListGrid, but rather a JavaScript Object containing
// properties for later construction of a ListGrid, use the <code>propertiesOnly</code>
// attribute.  For example, this code would cause the property "listGridProperties" to be a
// JavaScript Object with properties "width" and "height", whose values would be JavaScript
// Numbers.
// <pre>
// &lt;Canvas&gt;
//     &lt;listGridProperties xsi:type="ListGrid" propertiesOnly="true"
//                          width="500" height="600"/&gt;
// &lt;/Canvas&gt;
// </pre>
// For your reference: "xsi" stands for "XML Schema Instance"; this notation derives from XML
// Schema standards for explicitly specifying type inline.
// <P>
// <h3>Custom Components</h3>
// <P>
// If you use +link{classMethod:ClassFactory.defineClass(),defineClass()} to define a new component class
// "MyListGrid" which is a subclass of the built-in component ListGrid, you can create it in
// XML as shown below:
// <pre>
// &lt;ListGrid constructor="MyListGrid" width="500"/&gt;
// </pre>
// By using the &lt;ListGrid&gt; tag you advertise that properties should be interpreted
// as <code>ListGrid</code> properties.  By specifying <code>constructor</code>
// you tell SmartClient what class to +link{Class.create,create()}.
// <P>
// <h3>Component Schema</h3>
// <P>
// Instead of using the <code>constructor</code> and <code>xsi:type</code> attributes for
// custom components and custom properties, you can create a +link{group:componentSchema} that
// describes the custom component.  Declaring a component schema allows you to use your
// component just like the built-in SmartClient components, and also allows your component to
// be used within +link{group:visualBuilder}.
// </smartclient>
//
// <smartgwt>
// <h3>Custom Components</h3>
// <p>
// If you define a new component class <code>com.mycompany.MyListGrid</code> which is a subclass of the
// built-in component <code>ListGrid</code>, and you +link{group:reflection,register your class for reflection},
// you can create it in XML as shown below:
// <pre>
//    &lt;ListGrid constructor="com.mycompany.MyListGrid" width="500"/&gt;
// </pre>
// <p>
// By using the &lt;ListGrid&gt; tag you advertise that properties should be interpreted
// as <code>ListGrid</code> properties.  By specifying <code>constructor</code>
// you tell SmartGWT what class to create.
// <p>
// <h3>Custom Properties</h3>
// <p>
// Your custom component (e.g. <code>com.mycompany.MyListGrid</code>) may have additional
// properties which are not present in the standard superclass (e.g. <code>ListGrid</code>).
// You can set such properties in XML as if they were pre-defined properties:
// <pre>
//    &lt;ListGrid constructor="com.mycompany.MyListGrid" myProperty="false"/&gt;
// </pre>
// <p>
// In this case, the {@link com.smartgwt.client.bean.BeanFactory} code
// will ultimately call <code>MyListGrid.setMyProperty(false);</code> in order
// to set the property. Since <code>BeanFactory</code> knows that the property
// takes a boolean, it will automatically convert the string value "false" to a
// boolean, using the type conversions described below.
// <p>
// Instead of relying on the automatic type conversions, you can force a
// property to be interpreted as a given type by using the "xsi:type"
// attribute:
// <pre>
// &lt;ListGrid&gt;
//     &lt;constructor&gt;com.mycompany.MyListGrid&lt;/constructor&gt;
//     &lt;myProperty xsi:type="xsd:boolean"&gt;false&lt;/myProperty&gt;
// &lt;/ListGrid&gt;
// </pre>
// <p>
// The same notation works when you want to declare that an entire subobject has a given type.
// For example, this would cause the custom property "myListGrid" to have a live
// +link{ListGrid} instance as its value.  All of the properties on the &lt;myListGrid&gt; tag
// will be correctly interpreted as ListGrid properties and have the correct types.
// <pre>
// &lt;Canvas&gt;
//     &lt;myListGrid xsi:type="ListGrid" width="500" height="600"/&gt;
// &lt;/Canvas&gt;
// </pre>
// <p>
// For your reference: "xsi" stands for "XML Schema Instance"; this notation derives from XML
// Schema standards for explicitly specifying type inline.
// <p>
// <h3>Component Schema</h3>
// <p>
// Instead of using the <code>constructor</code> and <code>xsi:type</code> attributes for
// custom components and custom properties, you can create a +link{group:componentSchema} that
// describes the custom component.  Declaring a component schema allows you to use your
// component just like the built-in SmartGWT components, and also allows your component to
// be used within +link{group:visualBuilder}.
// <p>
// <h3>Type Conversions</h3>
// <p>
// The {@link com.smartgwt.client.bean.BeanFactory} code
// uses a reflection-like mechanism to discern the type which a SmartGWT
// property requires, and automatically
// converts supplied values to the required type when possible. In cases where
// conversion is impossible, an {@link IllegalArgumentException} is
// thrown.
// <p>
// Where the setter for a property takes a <b>primitive type</b> (boolean,
// double, float, int, or long), any "null" value supplied will be converted to
// 0 (for the numeric types) or false (for boolean). Conversely, if the setter
// takes the boxed version of the type (Boolean, Double, etc.), any primitive
// value supplied will be auto-boxed. Note that byte, short and char properties
// are not currently handled.
// <p>
// Properties which take <b>numeric types</b> will convert other numeric
// types, as well as strings, using standard Java APIs (e.g.
// <code>Integer.valueOf()</code>).  Boolean "true" will be converted to 1, and false to 0.
// If the supplied value cannot be converted to the numeric type, an
// {@link IllegalArgumentException} will be thrown.
// <p>
// Properties which take a <b>Date</b> type will convert from strings using
// {@link com.smartgwt.client.util.DateUtil#parseInput(String)}.
// <p>
// Properties which take <b>Enum</b> types will convert from strings using <code>Enum.valueOf()</code>.
// However, any dashes ("-") in the string will be converted to underscores, and
// the string will be converted to upper-case if necessary. If the string does not match
// one of the <code>Enum</code> values, an {@link IllegalArgumentException} will be thrown.
// <p>
// Properties which take <b>Array</b> types will convert arrays where the individual
// values can be converted to the appropriate type. If a single value is supplied,
// it will be wrapped in an array.
// </smartgwt>
//
// @treeLocation Concepts/Visual Builder
// @title Component XML
// @visibility external
//<

// <smartgwt>
//> @groupDef reflection
//
// In order to specify a SmartGWT class as a constructor in
// +link{group:componentXML, Component XML} or
// +link{group:componentSchema, Component Schema},
// or for other purposes, such as for
// {@link com.smartgwt.client.docs.AutoChildUsage autoChildren} or for
// {@link com.smartgwt.client.data.DataSourceField#setEditorType(Class)},
// you must first register the class with the
// {@link com.smartgwt.client.bean.BeanFactory} reflection mechanism.
// You can do this in <code>onModuleLoad()</code>, or at some later point
// before the constructor is used.
// <p>
// If you want to register {@link com.smartgwt.client.widgets.Canvas}
// and all its subclasses found in the classpath (including your custom subclasses),
// you can use the {@link com.smartgwt.client.bean.BeanFactory.CanvasMetaFactory}
// interface to do this automatically:
// <blockquote><pre>
// GWT.create(BeanFactory.CanvasMetaFactory.class);</pre></blockquote>
// <p>
// Similarly, to register {@link com.smartgwt.client.widgets.form.fields.FormItem}
// and all its subclasses found in the classpath (including your custom subclasses),
// you can use the {@link com.smartgwt.client.bean.BeanFactory.FormItemMetaFactory}.
// <blockquote><pre>
// GWT.create(BeanFactory.FormItemMetaFactory.class);</pre></blockquote>
// <p>
// Alternatively, if only specific classes need to be instantiated and
// configured dynamically, you can register just those classes by using the
// {@link com.smartgwt.client.bean.BeanFactory.MetaFactory} interface instead.
// Usage is most easily explained with an example. First, you define an
// interface. (Note that it can be an inner interface.)
// <blockquote><pre>
// public interface MyMetaFactory extends BeanFactory.MetaFactory {
//     BeanFactory&lt;ListGrid&gt; getListGridFactory();
//     BeanFactory&lt;TileGrid&gt; getTileGridBeanFactory();
// }</pre></blockquote>
// ... and then you trigger the generation process:
// <blockquote><pre>
// GWT.create(MyMetaFactory.class);</pre></blockquote>
// <p>
// Each function in the interface you define will result in the creation of
// one registered <code>BeanFactory</code> ... so, in this case, we would end up with
// bean factories for <code>ListGrid</code> and <code>TileGrid</code>. The rules
// are as follows:
// <ol>
// <li>The interface must extend <code>BeanFactory.MetaFactory</code></li>
// <li>The return type for each function must be <code>BeanFactory</code>,
// with a generic type that is the class you want to register for reflection.</li>
// <li>The function must take no arguments.</li>
// <li>The name of the function doesn't matter.</li>
// </ol>
// <p>
// If there are only a limited number of classes which require dynamic
// configuration, it will save code size to use the
// {@link com.smartgwt.client.bean.BeanFactory.MetaFactory} interface to generate
// factories for those specific types, rather than using
// {@link com.smartgwt.client.bean.BeanFactory.CanvasMetaFactory} or
// {@link com.smartgwt.client.bean.BeanFactory.FormItemMetaFactory}. Once a factory
// is generated for a class, GWT's opportunities to prune dead code are more
// limited for that class, since it cannot know what properties will be set or
// retrieved at run-time.

// @title Registering Classes for Reflection
// @visibility external
//<
// </smartgwt>

//> @groupDef componentSchema
//
// A component schema is a special type of DataSource that describes a custom component.
// <P>
// Declaring a component schema for your custom component allows you to:
// <ul>
// <li> use simpler XML when creating your custom component: avoid having to specify the
// <code>constructor</code> and <code>xsi:type</code> attributes as described under
// +link{group:componentXML}
// <li> use your custom component within +link{group:visualBuilder}
// </ul>
// <P>
// <b>Example of a Component Schema</b>
// <P>
// In its most basic form, a component schema for a custom subclass of ListGrid called
// "<smartgwt>com.mycompany.</smartgwt>MyListGrid" looks like this:
// <pre>
// &lt;DataSource serverType="component" ID="MyListGrid"
//             inheritsFrom="ListGrid" instanceConstructor="<smartgwt>com.mycompany.</smartgwt>MyListGrid"/&gt;
// </pre>
// With this definition saved as "MyListGrid.ds.xml" in the project dataSources directory
// ([webroot]/shared/ds/ by default), you can now create an instance of
// <smartgwt>com.mycompany.</smartgwt>MyListGrid with just:
// <pre>
// &lt;MyListGrid width="500"/&gt;
// </pre>
// Note: you may need to restart your servlet engine/J2EE container before this example will
// work.
// <P>
// The attributes set directly on the DataSource tag have special meaning for a component
// schema definition:
// <ul>
// <li>+link{dataSource.serverType,serverType}="component" indicates this DataSource describes
// a component, as opposed to a SQL table or other data provider
// <li>+link{dataSource.ID,ID} means the tagName that will be used to create your custom
// component.  This must match the first component of the filename. (ID="MyListGrid" means the filename
// must be MyListGrid.ds.xml, and typically also matches the name of the class).
// <li>+link{dataSource.inheritsFrom}="ListGrid" inherits the ListGrid property definitions via
// +link{dataSource.inheritsFrom}.
// <li>instanceConstructor="<smartgwt>com.mycompany.</smartgwt>MyListGrid" indicates the
// <smartclient>SmartClient</smartclient><smartgwt>SmartGWT</smartgwt> class that
// <smartclient>+link{Class.create,create()}</smartclient> should be called on to construct
// an instance.
// <smartclient>
// <li>showLocalFieldsOnly is a boolean that, when set to true, tells the +link{group:visualBuilder}
// to show only the fields declared in this schema in the component editor.  Otherwise fields
// inherited via +link{dataSource.inheritsFrom} (all the way up the chain) are also included.
// <li>showSuperClassEvents is a boolean that, like showLocalFieldsOnly, optionally restricts
// the list of events shown in the Events tab of the +link{group:visualBuilder} to those defined in
// this schema only.
// <li>showSuperClassActions is a boolean that optionally restricts the list of actions shown
// in the menu when you map a component Event to a component Action within +link{group:visualBuilder}
// to those defined in this schema only.
// </smartclient>
// </ul>
// <P>
// <b>Declaring custom properties</b>
// <P>
// Custom properties are declared via +link{dataSource.fields,fields} as for an ordinary
// +link{DataSource}.  As with ordinary DataSources, it is legal to redeclare inherited fields
// in order to modify properties such as +link{dataSourceField.editorType,field.editorType}.
// <P>
// The following DataSourceField properties have special significance when a component schema
// is used to process +link{group:componentXML,component XML}:
// <ul>
// <li> +link{dataSourceField.type,field.type} declares the type of the field, and hence the
// type of the <smartclient>JavaScript</smartclient> value your custom class will be
// initialized with.  In order to declare subcomponents, can be set to the name of another component
// (built-in or custom).
// <li> +link{dataSourceField.multiple,field.multiple} declares that the field should always be
// array-valued even when only a single value is provided.  Also indicates that the field name
// should be used as a "wrapper tag" in the XML format for the component.
// <li> +link{dataSourceField.propertiesOnly,field.propertiesOnly}.  For fields that hold
// subcomponents, suppresses auto-construction to avoid +link{canvas.autoDraw,double drawing}
// and to allow subcomponents to be modified by their parent component before they are created
// and drawn
// </ul>
// When a component is edited within Visual Builder, the DataSource properties that normally
// influence databound forms will influence the Component Editor (for example, field.title,
// field.editorType).  In addition, the following properties have special significance in
// component editing and component drag and drop:
// <ul>
// <li> +link{dataSourceField.inapplicable,field.inapplicable} indicates that an inherited
// field is inapplicable in this component.
// <li> +link{dataSourceField.group,field.group} indicates what group the property should be
// placed in when editing in Visual Builder.
// <li> +link{dataSourceField.xmlAttribute,field.xmlAttribute}: indicates this field should
// serialize as an XML attribute.  Note that when constructing the component from XML, either
// an attribute or a subelement will continue to be accepted as means of specifying the field
// value, so this property is primarily set in order to make code generated by Visual Builder
// maximally compact or to make it conform to externally set standards.
// </ul>
// <P>
//
// <smartclient>
// <b>Declaring Events and Actions</b>
// <P>
// Events and Actions are declared via a methods array.  In order for a method to be considered
// an event, it needs to have a method definition in the methods array (or be publicly
// documented in the SmartClient reference) and have been added to
// the class as a +link{group:stringMethods,StringMethod} via +link{Class.registerStringMethods}.
// <p>
// In order for a method to be considered an action, it needs to have a method definition in
// the methods array and have the <code>action</code> property set to <code>true</code>.  For
// example, the following is a definition of the 'hide' action available on any Canvas, as
// copied from Canvas.ds.xml:
// <pre>
//     &lt;methods&gt;
//         &lt;method name="hide" title="Hide" action="true"/&gt;
//     &lt;/methods&gt;
// </pre>
// For custom component actions, an array of expected parameters may be specified using the
// <code>params</code> attribute. Each entry in this array should have a specified type.
// By doing this, you allow the visual builder to pass parameters through to actions when setting
// up events that call actions (possibly on another component).
// For example if you had a component with a custom action that expected to be passed a single
// parameter of type +link{class:ListGridRecord} you could define it as follows:
// <pre>
//     &lt;method name="showRecordDetails" title="Show Record Details" action="true"&gt;
//         &lt;params&gt;
//             &lt;param type="ListGridRecord"/&gt;
//         &lt;params&gt;
//     &lt;/method&gt;
// </pre>
// If a user working within the visualBuilder then added ListGrid to the page and used the "+" icon
// to associate the +link{ListGrid.recordClick(),recordClick} event with this custom method, the
// Visual Builder logic would automatically associate the parameters available in the fired event
// (in this case <code>recordClick</code>) with the expected parameters for the action to fire
// by type. So the <code>record</code> parameter of the event (known to be of type
// <code>ListGridRecord</code>) would be passed through to this custom <i>showRecordDetails</i>
// action as the first parameter.
// </smartclient>
//
// @treeLocation Concepts/Visual Builder
// @title Component Schema
// @visibility external
//<

});

// shortcut
isc.xml = isc.XML = isc.XMLTools;






//> @class DataSource
// A DataSource is data-provider-independent description of a set of objects
// that will be loaded, edited and saved within the user interface of your application.
// <P>
// Each DataSource consists of a list of +link{dataSource.fields,fields} that make up a
// DataSource <code>record</code>, along with +link{dataSourceField.type,field types},
// +link{dataSourceField.validators,validation rules},
// +link{dataSourceField.foreignKey,relationships} to other DataSources, and other
// metadata.
// <P>
// The abstract object description provided by a DataSource is easily mapped to a variety of
// backend object models and storage schemes.  The following table shows analogous terminology
// across systems.
// <table border=1 class="normal">
// <tr>
//   <td>Isomorphic SmartClient</td>
//   <td>Relational Database</td>
//   <td>Enterprise Java Beans (EJB)</td>
//   <td>Entity/Relationship Modeling</td>
//   <td>OO/UML</td>
//   <td>XML Schema/WSDL</td>
//   <td>LDAP</td>
// </tr><tr>
//   <td>DataSource</td>
//   <td>Table</td>
//   <td>EJB class</td>
//   <td>Entity</td>
//   <td>Class</td>
//   <td>Element Schema (ComplexType)</td>
//   <td>Objectclass</td>
// </tr><tr>
//   <td>Record</td>
//   <td>Row</td>
//   <td>EJB instance</td>
//   <td>Entity instance</td>
//   <td>Class instance/Object</td>
//   <td>Element instance (ComplexType)</td>
//   <td>Entry</td>
// </tr><tr>
//   <td>Field</td>
//   <td>Column</td>
//   <td>Property</td>
//   <td>Attribute</td>
//   <td>Property/Attribute</td>
//   <td>Attribute or Element (SimpleType)</td>
//   <td>Attribute</td>
// </tr></table>
// <P>
// DataSources can be +link{group:dataSourceDeclaration,declared} in either JavaScript or XML
// format, and can also be +link{group:metadataImport,imported} from existing metadata formats,
// including XML Schema.
// <P>
// <i>Data Binding</i> is the process by which
// +link{dataBoundComponent,Data Binding-capable UI components} can automatically configure
// themselves for viewing, editing and saving data described by DataSources.  DataBinding is
// covered in the +docTreeLink{QuickStartGuide,QuickStart Guide}, Chapter 6, <i>Data
// Binding</i>.
// <P>
// +link{group:clientServerIntegration,Data Integration} is the process by which a DataSource
// can be connected to server systems such as SQL DataBases, Java Object models, WSDL web
// services and other data providers.  Data Integration comes in two variants: client-side and
// server-side.  +link{group:serverDataIntegration,Server-side integration} uses the
// SmartClient Java-based server to connect to data represented by Java Objects or
// JDBC-accessible databases.  +link{group:clientDataIntegration,Client-side integration}
// connects SmartClient DataSources to XML, JSON or other formats accessible via HTTP.
// <P>
// DataSources have a concept of +link{group:dataSourceOperations,4 core operations} ("fetch",
// "add", "update" and "remove") that can be performed on the set of objects represented by a
// DataSource.  Once a DataSource has been integrated with your data store, databinding-capable
// UI components can leverage the 4 core DataSource operations to provide many complete user
// interactions without the need to configure how each individual component loads and saves
// data.
// <P>
// These interactions include +link{listGrid,grid views}, +link{TreeGrid,tree views},
// +link{detailViewer,detail views}, +link{DynamicForm,form}-based
// +link{dynamicForm.editRecord,editing} and +link{dynamicForm.saveData,saving},
// grid-based +link{listGrid.canEdit,editing} and +link{listGrid.saveByCell,saving},
// and custom interactions provided by +explorerExample{patternReuse} custom
// databinding-capable components.
//
// @see interface:DataBoundComponent for information on DataBound Components
// @see group:dataSourceDeclaration for how to create DataSources
// @treeLocation Client Reference/Data Binding
// @serverDS allowed
// @visibility external
//<
isc.defineClass("DataSource");

//> @type DSOperationType
// One of the four basic operations that can be performed on DataSource data: "fetch",
// "add", "update", "remove".  Elsewhere called CRUD operations, where CRUD stands for
// "create", "retrieve", "update", "delete", which correspond to "add", "fetch", "update" and
// "remove" in SmartClient terminology.  See +link{group:dataSourceOperations} for a full
// description.
// <p>
// There are also additional, non-CRUD operations explained below.
//
//
// @value "fetch"        Fetch one or more records that match a set of search criteria.
// @value "add"          Store new records
// @value "update"       Update an existing record
// @value "remove"       Remove (delete) an existing record
// @value "custom"       perform some arbitrary custom logic that is not a CRUD operation.
//                       Format of the inputs and outputs is unconstrained, and the operation
//                       will be ignored for cache sync purposes by +link{ResultSet}s.  See
//                       +link{DataSource.performCustomOperation()}.
// @value "validate"     Run server-side validation for "add" or "update" without actually
//                       adding or updating anything.  See +link{DataSource.validateData()}.
// @value "viewFile"     Retrieve a file stored in a binary field in a DataSource record, and
//                       allow the browser to choose whether to view it directly or prompt the
//                       user to save.  See +link{group:binaryFields}.
// @value "downloadFile" Like "viewFile", but the HTTP header Content-Disposition is used to
//                       suggest that the browser show a save dialog.  See +link{group:binaryFields}.
// @value "clientExport" Upload formatted client data and export it to Excel, XML and other
//                       formats.  Used automatically by
//                       +link{method:dataSource.exportClientData(),exportClientData()}
//                       and cannot be used directly.  Usable only with the SmartClient server
//                       framework.
// @visibility external
//<

//> @object Record
// A Record is an ordinary JavaScript Object with properties that are treated as data to
// be displayed and edited by a +link{DataBoundComponent}.
// <P>
// +link{DataBoundComponent}s have a concept of +link{dataBoundComponent.fields,named fields},
// where values for each field are found under the same-named property in a Record.
// <P>
// A Record is always an ordinary JavaScript Object regardless of how the record is
// loaded (static data, java server, XML web service, etc), and so supports the
// normal behaviors of JavaScript Objects, including accessing and assigning to properties
// via dot notation:
// <pre>
//     var fieldValue = record.<i>fieldName</i>;
//     record.<i>fieldName</i> = newValue;
// </pre>
// <P>
// The concept of working with Records is common to all +link{DataBoundComponent}s, although
// individual DataBoundComponents may work with singular records (+link{DynamicForm}) or
// may work with lists (+link{ListGrid}), trees (+link{TreeGrid}), or cubes
// (+link{CubeGrid}) of records.
// <P>
// Individual DataComponents may also look for special properties on Records which
// control styling or behavior for those records, such as
// +link{listGrid.recordEditProperty,<code>record.canEdit</code>}.
//
// @treeLocation Client Reference/Data Binding/DataSource
// @visibility external
//<

//> @groupDef dataSourceDeclaration
// DataSources can be specified in XML format, in which case the SmartClient server is used to
// load the DataSource, or DataSources can be programmatically created on the client.
// <P>
// Whether a DataSource is loaded via the SmartClient server or programmatically created
// client-side, identical requests will ultimately be submitted to the server.  However,
// DataSources defined in XML are loaded and used by the SmartClient Server, enabling many
// features including synchronized client-server validation, request bundling, file upload, and
// optional automatic SQL/JPA/Hibernate connectivity (see the
// +link{group:iscServer,Server Summary} for details).
// <P>
// <smartclient>
// DataSources created on the client use the same style of creation as DataBound components:
// <pre>
//    isc.DataSource.create({
//        ID:"supplyItem",
//        fields:[
//            {name:"itemName", ... }
//            ...
//        ]
//    });
// </pre>
// </smartclient>
// <smartgwt>
// DataSources can be programmatically created on the client like so:
// <pre>
//    DataSource ds = new DataSource();
//    ds.setID("supplyItem");
//    DataSourceTextField nameField = new DataSourceTextField("itemName", "Name");
//    // create other fields
//    ds.setFields(nameField, ...);
// </pre>
// </smartgwt>
// Reference for all properties that can be set for DataSources, their fields and validators is
// given in the +link{class:DataSource} class reference.
// <P>
// DataSources defined in XML declare fields, validators and other settings using XML tags:
// <pre>
//     &lt;DataSource ID="supplyItem"&gt;
//         &lt;fields&gt;
//             &lt;field name="itemName" type="text" title="Item"/&gt;
//             &lt;field name="SKU"      type="text" title="SKU"&gt;
//                 &lt;validators&gt;
//                     &lt;validator type="integerRange" ... /&gt;
//                 &lt;/validators&gt;
//             &lt;/field&gt;
//         &lt;/fields&gt;
//     &lt;/DataSource&gt;
// </pre>
// DataSources defined in XML are loaded by using the <code>DataSourceLoader</code>
// servlet provided by the SmartClient Server.  This can be done as an ordinary HTML
// &lt;script&gt; tag as you application first loads:
// <pre>
//     &lt;SCRIPT SRC=isomorphic/DataSourceLoader?dataSource=supplyItem,employees,worldDS&gt;&lt;/SCRIPT&gt;
// </pre>
// .. or can be done on the fly via +link{DataSource.load()}.
// <P>
// Alternatively, in JSP environments, XML DataSources can be loaded via a special JSP tag
// supported by the SmartClient Server:
// <pre>
//     &lt;%&#64; taglib uri="/WEB-INF/iscTaglib.xml" prefix="isomorphic" %&gt;
//     ...
//     &lt;SCRIPT&gt;
//     &lt;isomorphic:loadDS ID="supplyItem"/&gt;
//     &lt;/SCRIPT&gt;
// </pre>
// <P>
// When loading an XML DataSource, by default, the ISC Server will look for a file named
// <code>&lt;dataSourceId&gt;.ds.xml</code> in the <code>/shared/ds</code> subdirectory under
// webroot.  The location of this directory can be changed, or individual DataSources can be
// placed in arbitrary locations.  For more information, see
// +link{group:server_properties,[webroot]/WEB-INF/classes/server.properties}.
// <P>
// XML DataSources can also be generated on the fly in case the entire DataSource or portions
// of it are based on dynamic data.  See the server API
// com.isomorphic.DataSource.addDynamicDSGenerator().
//
// @see class:DataSource
// @see group:loadDSTag
// @treeLocation Client Reference/Data Binding/DataSource
// @title Creating DataSources
// @visibility external
//<



//> @groupDef i18n
//
// <b><u>Internationalizing SmartClient Applications</u></b>
// <p>
// Internationalizing a SmartClient application involves creating locale-specific
// versions of all strings, images, and possibly colors.  In some cases, component layout may
// be affected as well (for example date field order differs between some locales).
// <p>
// <u><b>Character Encodings</b></u>
// <p>
// In order to deliver content in different languages to the browser and have it be displayed
// correctly, it is important that the browser and server agree on the character encoding being
// used to send the page.
// <p>
// Generally speaking, you can use whatever character encoding you prefer, so long as
// you're consistent about serving your files to the browser with exactly the same encoding
// as was used to save the file to disk.  Keep in mind though that the character encoding you
// choose must be supported by all browsers you intend to support.  Isomorphic recommends that,
// where possible, you use the UTF-8 encoding.  Regardless of which character encoding you
// choose, keep in mind that for the browser to correctly display non-ASCII characters, you
// <b>must</b> explicitly set the character encoding of your page to match the encoding you
// used to save the file.  Browsers have built-in heuristics to guess the character encoding,
// but they can't be relied upon to work 100% of the time.
// <p>
// There are two ways to explicitly tell the browser what character
// encoding is being used for a given page.  One is to set the "Content-Type" HTTP header, for
// example:
// <pre>
// Content-Type: text/html; charset=UTF-8
// </pre>
// If you're using JSP on the back-end, you can set this header as follows:
// <pre>
// &lt;%@ page contentType="text/html; charset=UTF-8"%&gt;
// </pre>
// Or using Java Servlet APIs in a Servlet as follows:
// <pre>
// response.setContentType("text/html; charset=UTF-8");
// </pre>
// Note that the latter needs to be done before any content is written to the response output
// stream.
// <p>
// The other approach to setting the content encoding is to use an HTML META tag to embed it
// in the page itself (note that this applies to HTML documents only).  The META tag must go
// inside the &lt;HEAD&gt; HTML element - e.g. as follows:
// <pre>
// &lt;HEAD&gt;
// &lt;META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"&gt;
// &lt;/HEAD&gt;
// </pre>
// In addition, for a .html or other static (non-.jsp) file, you must also ensure that the file
// itself is saved in UTF-8 encoding.  Advanced text editors (including the Eclipse IDE) can
// show you the encoding of a text file and allow you to change it.  In Eclipse this is in the
// Preferences area (search for "Encoding") and UTF-8 is <b>not the default</b> for .html
// files, and should be changed.
// <P>
// Isomorphic recommends that you follow <b>all</b> of the above approaches for maximum
// compatibility.  For example, if you omit the META tag approach, pages saved by the user
// using the browser save function may not render correctly when subsequently read from disk
// because HTTP headers are not available in that context.  Conversely, not providing the
// HTTP header or not saving the file in UTF-8 can result in application servers delivering the
// file in the encoding in which it is saved, or in their own default and incorrect content
// encoding.
// <p>
// If you're using a given character encoding pervasively in your pages, you can also configure
// your web server or application server to use that character encoding as the default for all
// pages of a given mime type or some other criteria (depending on the capability of your
// server) - for example on a per-directory basis.
// <p>
// For more information on character encodings, character sets, and working with HTML, please
// see W3C's tutorial here:
// +externalLink{http://www.w3.org/International/tutorials/tutorial-char-enc/}
// <p>
// <b>NOTE: Default Encoding</b>
// <p>
// As mentioned above, Isomorphic recommends the use of UTF-8 encoding.  However, it is
// not possible to configure some servlet engines to do this by default.  Both Tomcat and Jetty
// default to the ISO-8859-1 character set (commonly known as Latin-1), even if the Java VM
// specifies a different encoding (the Tomcat documentation claims that this behavior is a
// requirement of the Servlet specification).
// <p>
// To work around this, we change charset encoding to UTF-8 on every request/response that
// goes through two core Isomorphic servlets: <code>IDACall</code> and <code>DataSourceLoader</code>.
// If you need to change this, you can do so by adding <code>init-param</code> settings to your
// <code>web.xml</code> file.  Please see the Javadocs for those two servlets for examples of
// how to do this.
// <p>
// <u><b>Framework Message Localization</b></u>
// <p>
// SmartClient components include standard prompts and error messages in various cases, and all
// such messages support localization.
// <P>
// <smartclient>
// Language packs are included in the directory smartclientSDK/isomorphic/locales.  To load a
// language pack, simply include the desired locale via a
// standard HTML &lt;script src=&gt; tag, for example:
// <pre>
//    &lt;SCRIPT SRC="isomorphic/locales/frameworkMessages_pt.properties"&gt;&lt;/SCRIPT&gt;
// </pre>
// .. to load the Portuguese locale. To see various locales, have a look at the
// +explorerExample{changeLocales,Localization example}.
// </smartclient>
// <smartgwt>
// SmartGWT ships with pre-built language packs for many languages.  These are automatically
// used by the framework: all you have to do is ensure that the appropriate locale has been
// set using one of the
// <a href=https://developers.google.com/web-toolkit/doc/latest/DevGuideI18nLocale#LocaleSpecifying>standard GWT mechanisms</a>.
// In brief, either:<ul>
// <li>Add a meta tag to your bootstrap page: <code>&lt;meta name="gwt:property" content="locale=ja_JP"&gt;</code></li>
// <li>Specify the locale in a query parameter: <code>http://www.example.org/myapp.html?locale=fr_CA</code></li>
// </ul>
// </smartgwt>
// <P>
// If you find that the language pack you are using has any incorrect or missing translations,
// or you want to add a new language pack, please visit
// +externalLink{http://forums.smartclient.com/showthread.php?t=9987,this forums thread} for
// instructions on how to contribute translations so that they will be added to future
// SmartClient builds.
// <P>
// You can alternatively maintain your own private additions or overrides to the default
// language packs.
// <smartclient>
// A complete list of standard messages is presented +link{group:i18nMessages, here}.<br>
// To customize these messages, use +link{Class.addProperties()} or
// +link{Class.addClassProperties()} to override default values at the instance or class level
// respectively.  For example, the class property
// +link{classAttr:RPCManager.timeoutErrorMessage} is displayed to the user in a warning dialog
// when a SmartClient operation fails to complete.  This can be modified using the
// +link{Class.addClassProperties()} method, as follows:
// <pre>
// isc.RPCManager.addClassProperties({timeoutErrorMessage:"Custom Timeout Message"});
// </pre>
// </smartclient>
// <smartgwt>
// Use the standard GWT technique for selectively overriding Messages: create a
// subclass of <code>SmartGwtMessages</code> that overrides specific methods, and a
// .properties file with your overridden messages.  Then create your overridden Messages
// instance and call <code>I18nUtil.initMessages()</code> with it.  For example:
// <pre>
//    public class CustomizedSgwtMessages extends com.smartgwt.client.i18n.SmartGwtMessages {
//       // override button_title
//       String button_title();
//    }
//
//    // then in a new file CustomizedSgwtMessages.properties
//    button_title=My Title
//
//    // and in a new file CustomizedSgwtMessages_fr.properties
//    button_title=French translation
//
//    // then in your onModuleLoad() call
//    CustomizedSgwtMessages myMessages = GWT.create(CustomizedSgwtMessages);
//    I18nUtil.initMessages(myMessages);
// </pre>
// </smartgwt>
// <p>
// <u><b>Application Message Localization</b></u>
// <p>
// <smartclient>
// Text such as button titles typically appears in the middle of JavaScript code:
// <pre>
//     isc.Button.create({ title: "Click me!"});
// </pre>
// If you are on the Java platform and your JavaScript code appears in a .jsp, you can use the
// JSTL Core package (Java Server Pages Standard Tag Library) to replace that static "Click
// me!" text with a localized reference.  This can be done as follows:
// <pre>
//     &lt;%@ taglib prefix="fmt" uri="/WEB-INF/fmt.tld" %&gt;
//     &lt;SCRIPT&gt;
//     isc.Button.create({ title: &lt;fmt:message key="buttonTitle"&gt; });
//     &lt;/SCRIPT&gt;
// </pre>
// Assuming you've set the current Locale and provided a resource bundle with the relevant
// translated text, the above would create a button with a title that comes from the locale-specific
// bundle you provided.  For more information, see Sun's i18n tutorial:
// +externalLink{http://java.sun.com/docs/books/tutorial/i18n/} and the JSTL home page:
// +externalLink{http://java.sun.com/products/jsp/jstl/}.
// <P>
// Note that this approach does not imply that your JavaScript must appear in the midst of an
// HTML page.  A .jsp file can be used to return a pure JavaScript response via just removing
// the &lt;SCRIPT&gt; tags in the example above, and targetting the .jsp with a &lt;SCRIPT
// SRC=&gt; tag from another .jsp or .html file.
// <P>
// A second common technique is to load internationalized messages as a JavaScript Object
// mapping from a message name to the internationalized message.  For example, if your messages
// are stored in Java .properties format, you could load your .properties file as a standard Java
// +externalLink{http://docs.oracle.com/javase/6/docs/api/java/util/Properties.html,Properties object},
// and use the server-side com.isomorphic.js.JSTranslater.toJSVariable() API to output the
// properties as a JavaScript variable.
// <P>
// If the variable name chosen was 'i18nMessages', code to make use of the .properties data
// would look like:
// <pre>
//     isc.Button.create({ title: i18nMessages["buttonTitle"] });
// </pre>
// <P>
// Approaches for non-Java platforms are similar, and generally any approach that works for
// HTML or JavaScript files works with SmartClient.  See the end of this section for pointers
// to additional resources.
// </smartclient>
// <smartgwt>
// String localization in SmartGWT is best done using <a href=http://code.google.com/webtoolkit/doc/latest/DevGuideI18n.html>
// standard GWT approaches</a>.  Although GWT supports various ways of localizing strings,
// Isomorphic recommends the use of an approach based on Resource Bundles.  Other parts of
// SmartGWT - for example, <code>.ds.xml</code> files - are best localized using resource
// bundles, so using resource bundles makes it easier to share messages that are used both
// client and server side.
// </smartgwt>
// <p>
// <u><b>Support for Right-to-Left (RTL) languages</b></u>
// <P>
// SmartClient includes support for RTL languages.  To enable, simply set
// <code>dir="rtl"</code> on the HTML element:
// <pre>
//    &lt;HTML dir="rtl"&gt;
// </pre>
// ListGrid columns, horizontal layouts, scrolling directions, etc will reverse order
// automatically.
// <p>
// Because RTL in some cases requires additional media files, RTL is only supported for the
// Enterprise, EnterpriseBlue and Graphite skins.
// <p>
// <u><b>DataSource and Component XML localization</b></u>
// <p>
// Please see the separate article on
// +link{group:dataSourceLocalization,DataSource and Component XML Localization}
// <p>
// <u><b>Localized Number and Currency Formatting</b></u>
// <p>
// Please see the separate article on
// +link{group:localizedNumberFormatting,Localized Number Formatting}
// <p>
// <smartclient>
// <u><b>Image, CSS localization</b></u>
// <p>
// Most SmartClient components use a mixture of text, CSS and images to render.  If you wish to
// provide locale-specific images or use locale-specific CSS, you can create localized copies
// of a SmartClient skin named after the locale and modify images as you see fit.  A skin is
// specified at load time using either the <code>skin</code> attribute of the
// <code>&lt;isomorphic:loadISC&gt;</code> tag or by loading the skin
// directly using a script tag like so: &lt;SCRIPT SRC=/isomorphic/skins/[skin]/load_skin.js&gt;.
// If you're using the <code>loadISC</code> tag, you can specify a locale-specific skin like so:
// <pre>
// &lt;isomorphic:loadISC skin="[localeName]"/&gt;
// </pre>
// Or you can use a script tag like so:
// <pre>
// &lt;SCRIPT SRC=/isomorphic/skins/[localeName]/load_skin.js&gt;&lt;/SCRIPT&gt;
// </pre>
// Where the <code>[localeName]</code> is the name of the current locale - you'll have this in the
// current page context as you'll need it to use the JSTL &lt;fmt:setLocale&gt; tag.
// <p>
// </smartclient>
// Resources:
// <p>
// <b><u>Java</u></b>
// <ul>
// <li>Sun's i18n tutorial: +externalLink{http://java.sun.com/docs/books/tutorial/i18n/}
// <li>JSTL home page: +externalLink{http://java.sun.com/products/jsp/jstl/}
// <li>Apache JSTL "Getting Started":
//     +externalLink{http://jakarta.apache.org/taglibs/doc/standard-doc/standard/GettingStarted.html}
// <li>Apache taglibs: +externalLink{http://jakarta.apache.org/taglibs/}
// </ul>
// <p>
//
// <b><u>.NET</u></b>
// <ul>
// <li>MSDN Developer Resource: "Developing World-Ready Applications":
// +externalLink{http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondesigningglobalapplications.asp}
// </ul>
//
// <b><u>PHP</u></b>
// <ul>
// <li>PHP Gettext manual: +externalLink{http://us2.php.net/gettext}
// <li>O'Reilly Gettext tutorial: +externalLink{http://www.onlamp.com/pub/a/php/2002/06/13/php.html}
// </ul>
// <p>
//
// <b><u>Perl</u></b>
// <ul>
// <li>Perl, Unicode and i18n FAQ: +externalLink{http://rf.net/~james/perli18n.html}
// </ul>
// @treeLocation Concepts
// @title Internationalization and Localization
// @visibility external
//<





//> @groupDef dataSourceLocalization
// <smartclient>
// DataSources can be created in +link{group:dataSourceDeclaration,several ways}.
// DataSources created directly in JavaScript can be
// internationalized via the techniques described in the main +link{group:i18n,i18n article}.
// </smartclient>
// DataSources which are declared in XML (.ds.xml files) and are read by the SmartClient
// server, which are normally loaded <smartclient>into a .jsp page via the
// <code>&lt;isomorphic:loadDS&gt;</code> JSP tag,</smartclient>
// <smartgwt>by the <code>DataSourceLoader</code> servlet,</smartgwt> can instead be
// internationalized using an approach similar to the internationalization of JSP files with
// JSTL tags.  This approach is also supported for screens defined using
// +link{group:componentXML,Component XML}.
// <p>
// <b>Note: </b>The tags we use for internationalizing SmartClient XML files look like
// standard JSTL tags; this is intentional, simply because developers are familiar with JSTL.
// However, the tags are being processed by SmartClient code, <b>not</b> JSTL, so only the
// specific tags documented here are supported.
// <P>
// Given the following DataSource located in /shared/ds/supplyItem.ds.xml:
// <pre>
// &lt;DataSource ID="supplyItem"&gt;
//     &lt;fields&gt;
//         &lt;field name="itemName"&gt;
//             &lt;title&gt;Item Name&lt;/title&gt;
//             &lt;validators&gt;
//                 &lt;Validator type="lengthRange" max="40"&gt;
//                     &lt;errorMessage&gt;Must be 40 characters or less.&lt;/errorMessage&gt;
//                 &lt;/Validator&gt;
//             &lt;/validators&gt;
//         &lt;/field&gt;
//     &lt;/fields&gt;
// &lt;/DataSource&gt;
// </pre>
// To localize the title and validator error string of the <code>itemName</code> field,
// change the DataSource definition as follows:
// <pre>
// &lt;DataSource ID="supplyItem" xmlns:fmt="WEB-INF/"&gt;
//     &lt;fields&gt;
//         &lt;field name="itemName"&gt;
//             &lt;title&gt;&lt;fmt:message key="itemTitle"/&gt;&lt;/title&gt;
//             &lt;validators&gt;
//                 &lt;Validator type="lengthRange" max="40"&gt;
//                     &lt;errorMessage&gt;&lt;fmt:message key="itemLengthRangeValidator"/&gt;&lt;/errorMessage&gt;
//                 &lt;/Validator&gt;
//             &lt;/validators&gt;
//         &lt;/field&gt;
//     &lt;/fields&gt;
// &lt;/DataSource&gt;
// </pre>
// This will cause SmartClient Server to look for a
// <a href=http://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-java.util.Locale->ResourceBundle</a>
// called "supplyItem", containing keys "itemTitle" and "itemLengthRangeValidator", and
// replace the <code>&lt;fmt:message&gt;</code> tags with the values from the resource bundle
// in the expected way.  It obtains the user's <code>Locale</code> from the servlet request,
// but you can override this if you want to force an application-specific locale, regardless of
// the user's operating system settings.  To do this, <smartclient> either specify
// a "locale" attribute in the &lt;loadDS&gt; tag, like this:<br>
// <code>&lt;loadDS ID="myDataSource" locale="es" /&gt;</code><p>
// or </smartclient> specify a "locale" parameter on HTTP requests to the <code>DataSourceLoader</code>
// and <code>IDACall</code> servlets.
// <p>
// <smartclient> In both of these cases, the </smartclient>
// <smartgwt>The </smartgwt> locale parameter should be an underscore-separated string
// conforming to the rules described in
// <a href=https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html>this article</a>
// on Java internationalization.  For example, "fr" (French language) or "en_US" (English
// language, US location).
// <p>
// As mentioned, SmartClient Server will look for a <code>ResourceBundle</code> called
// "supplyItem" in this case because it defaults to the name of the DataSource.  It is
// possible to override this default at both the DataSource and field levels:
// <ul>
// <li>Specify <code>&lt;fmt:bundle&gt;</code> as a top-level DataSource tag, like this:<br>
// <pre>   &lt;DataSource  xmlns:fmt="WEB-INF/" ID="supplyItem"&gt;
//      &lt;fmt:bundle basename="com.isomorphic.test.i18n" /&gt;
//      ...
// &lt;/DataSource&gt;</pre></li>
// <li>Specify the bundle name in the individual <code>&lt;fmt:message&gt;</code> tags, like this:<br>
// <pre>   &lt;title&gt;&lt;fmt:message key='title1' bundle="com.mycompany.MyProperties" /&gt;&lt;/title&gt;</pre></li>
// </ul>
// When you name a resource bundle manually like this, if you qualify the name it influences
// where we expect to find that resource bundle.  In the above example, we would look in the
// <code>com.mycompany</code> package.  For unqualified names (including the default of the
// DataSource name that we use in the absence of an override), we look in the so-called
// "default package", which corresponds to the root of your classes directory or the root of
// a .JAR file.
// <p>
// Note that the <code>xmlns:fmt</code> attribute in the DataSource definition is required by
// the XML parser if you intend to use our <code>fmt:message</code> features.  However, the
// actual value you use is unimportant as long as it is present.
// <p>
// Although these examples don't show it, note that it is also possible to internationalize
// DataSource-level values in the same way as field-level values - for example:<pre>
//   &lt;DataSource  xmlns:fmt="WEB-INF/" ID="i18nTest"&gt;
//     &lt;title&gt;&lt;fmt:message key="dsTitle" /&gt;&lt;/title&gt;
//      ...
// &lt;/DataSource&gt;
// </pre>
// <h4>Component XML</h4>
// All of the above applies similarly for localizing screens defined using
// +link{group:componentXML,Component XML}.  For example:<pre>
// &lt;isomorphicXML xmlns:fmt="WEB-INF/"&gt;
//   &lt;fmt:bundle basename="com.isomorphic.test.i18n.test" /&gt;
//   &lt;Window ID="testWin1"&gt;
//     &lt;title&gt;&lt;fmt:message key='title1' /&gt;&lt;/title&gt;
//   &lt;/Window&gt;
// &lt;/isomorphicXML&gt;
// </pre>
// Note the following differences between localizing <code>.ds.xml</code> (DataSource) files and
// localizing <code>.ui.xml</code> (component XML) files: <ul>
// <li>The <code>&lt;isomorphicXML&gt;</code> tag, which is ordinarily only used to wrap
// multiple widget definitions to give a valid XML file, is required if you are using this
// internationalization technique.</li>
// <li>Because there is no concept of an "ID" associated with a <code>.ui.xml</code> file,
// there is no default bundle like there is with DataSource definitions.  Instead, you have to
// specify the bundle by hand, either by adding a <code>&lt;fmt:bundle&gt;</code> tag as an
// immediate child of the <code>&lt;isomorphicXML&gt;</code> tag, or by specifying
// <code>bundle</code> attributes in your individual <code>&lt;fmt:message&gt;</code> tags.</li>
// <li>Locale can be overridden, as described for DataSources above, by
// specifying a "locale" parameter on HTTP requests to the <code>ScreenLoaderServlet</code>
// (this is done for you when you pass a locale to the +link{RPCManager.loadScreen} method).</li>
// </ul>
// @treeLocation Concepts/Internationalization and Localization
// @title DataSource and Component XML Localization
// @visibility external
//<

//> @groupDef localizedNumberFormatting
//
// Use the special +link{dataSourceField.type,field types} "localeInt", "localeFloat" and
// "localeCurrency" to have locale-specific formatting applied to numeric values, as well as
// automatic parsing of user inputs that use locale-specific decimal and grouping separators.
// <p>
// <smartclient>
// These special types rely on the settings +link{numberUtil.decimalSymbol},
// +link{numberUtil.groupingSymbol} and +link{numberUtil.currencySymbol}.  These are normally
// set automatically when you load a language pack for the current locale.
// <p>
// See +link{group:i18n} for more background on internationalization and loading language packs
// for the current locale.
// </smartclient>
// <smartgwt>
// These special types automatically use GWT's NumberFormat logic, using the default locale.
// Additional +link{SimpleType,SimpleTypes} can be created that call GWT's NumberFormat if more
// variations are needed.
// <p>
// Note that language packs shipped with SmartGWT contain properties such as "decimalSymbol"
// that seemingly would control localized number formatting.  These are inactive for SmartGWT
// and should be ignored.
// <p>
// See +link{group:i18n} for more background on internationalization.
// </smartgwt>
//
// @title Localized Number Formatting
// @visibility external
//<

//> @groupDef featureExplorerOverview
//
// The +docTreeLink{FeatureExplorer, Feature Explorer} is an example shell designed to help you
// explore the capabilities of SmartClient.  Read on for a brief overview, including specific
// instructions for using the example code in your own standalone application.
// <p>
// The tree on the left of the Feature Explorer contains examples grouped by logical
// categories.  Selecting an example brings it up on the right side of the screen, inside a
// tabbed view.  The default "View" tab shows the interactive example itself, while other
// tabs show the underlying source.  The "JS" tab shows the source for the example.  There is
// sometimes also an "XML" tab that shows the corresponding version in
// +link{componentXML, Component XML} format.  For databound examples, there are also
// frequently additional tabs that show the +link{dataSource, DataSources} associated with the
// example.
// <p>
// <b><u>How to create a standalone application using code from the Feature Explorer</u></b>
// <p>
// The Feature Explorer shell is designed to show many examples in one place and to enable
// experimentation by providing a way for you to modify example code and try out your changes.
// As a result, the Feature Explorer requires some basic server support and its examples omit
// the usual SmartClient module includes that have to be in place for using SmartClient
// components standalone.
// <p>
// If you'd like to use example code in your application to get started quickly, create a page
// with SmartClient includes and then take the code from the "JS" tab and place it between
// &lt;SCRIPT&gt; blocks as described +link{group:nonJavaBackend, here}.  If the example also
// includes a datasource, place the datasource definitions in the same file before the
// component code.  Note that DataSources (and components) written in XML require the optional
// SmartClient server.  If you're using the server, you can include them on your page using the
// +link{group:loadDSTag} tag.
// <p>
// <b><u>Feature Explorer difference in the LGPL package</u></b>
// <p>
// The LGPL edition of SmartClient does not include the SmartClient Java Server as part of the
// licensed software, but a trimmed down server is included in the package to support the
// Feature Explorer shell.  There are some examples that use DataSources that would normally
// use the SmartClient server for persistence.  In the LGPL package, these DataSources are
// automatically turned into +link{group:clientOnlyDataSources, Client Only DataSources} and
// the Feature Explorer loads the data for these one-time from the dataURL or testFileName
// attributes specified on the DataSource.  Subsequent DataSource operations work against this
// client-side dataset, which is why changes to the data aren't permanent in these examples.
// <p>
// Check out the +link{clientServerIntegration, Client-Server Integration} overview topic for
// an overview of your DataBinding options.
//
// @title Feature Explorer Overview
// @visibility external
//<

//> @groupDef iscInstall
//
// This overview serves as a how-to for installing SmartClient into your web application.
// Evaluators are urged to use the SmartClient SDK with the embedded tomcat servlet engine
// during evaluation rather than pursue installation into an existing web application up front,
// however, reading this document and the related +link{group:clientServerIntegration,server
// integration} materials is recommended to get an overview.
// <P>
// SmartClient has two pieces - the client components that run in the browser and the server
// components that run in a J2SE-compatible container.  You don't need to use a Java back-end
// to use SmartClient, but the SDK comes with some examples that assume the presence of the
// Java back-end and, for some examples, a SQL Database.  If you will be using SmartClient with
// a Java back-end, see below for the list of J2SE application servers supported by the Java
// implementation of the SmartClient server.
// <p>
// The SDK contains two top-level directories: <code>smartclientSDK</code> and
// <code>smartclientRuntime</code>.  The <code>smartclientSDK</code> directory contains the
// embedded servlet engine, embedded database, examples, and documentation.  The
// <code>smartclientRuntime</code> directory contains just the client and server components of
// the SmartClient product - use the contents of this directory when deploying SmartClient into
// your application environment.
// <p>
// <u><b>Client integration</b></u>
// <p>
// To install the client-side portion of SmartClient, simply copy the <code>isomorphic</code>
// directory from the smartclientRuntime webroot to the webroot of your application.  Having
// done this you can use SmartClient components on your pages regardless of the technologies
// used on your back-end and you can bind to server-side componentry backed by arbitrary
// technology - see the <i>Data Integration</i> section of the
// +link{group:clientServerIntegration} section for more information.
// <p>
// <u><b>Server integration</b></u>
// <p>
// <span style='color:red'>Note:</span> Some of the instructions below ask you to copy files
// into the WEB-INF/classes folder.  If you're using an IDE such as Eclipse that attempts to
// manage the WEB-INF/classes folder, we recommend that you copy these files to the src/
// directory of your project (next to the top-level folder for your java namespace) such that
// your IDE auto-deploys them to the WEB-INF/classes folder.  We have seen cases of tools like
// Eclipse periodically deleting files that are checked into to WEB-INF/classes directly.
// <p>
// <ul>
// <li> Copy all files from the WEB-INF/lib directory of the smartclientRuntime to your
// WEB-INF/lib.  The set of libs in the smartclientRuntime/WEB-INF/lib folder is a minimal set;
// smartclient<b>SDK</b>/WEB-INF/lib contains all other .jars you might need, including third-party
// libraries bundled for convenience.  See
// +link{group:javaModuleDependencies,Java Module Dependencies} for details of the .JAR files that
// comprise the SmartClient Server, and their dependencies on various third-party libraries.
// Generally, if there are conflicts with the versions of third-party libraries you want to
// use, you can use the versions you want - SmartClient has minimal dependencies on these
// libraries.
// <li> Copy the WEB-INF/classes/log4j.isc.config.xml from the smartclientRuntime to your
// WEB-INF/classes
// directory.  This file contains the SmartClient server log configuration.
// <li> Copy the +link{group:server_properties,WEB-INF/classes/server.properties}
// from the smartclientRuntime to your WEB-INF/classes
// directory.  This file contains settings for basic file locations such the location of
// webroot, the SmartClient SQL engine and DMI.  The version under smartclientRuntime has a
// basic, secure configuration.  See the version of +link{group:server_properties,server.properties}
// under the smartclientSDK directory for sample SQL and other settings.
// <li> Copy the WEB-INF/iscTaglib.xml from the smartclientRuntime to your WEB-INF directory.
// This file enables the use of custom SmartClient tags in your JSPs.
// <li> Merge portions of the WEB-INF/web.xml into your application's web.xml.  To use
// SmartClient JSP tags like &lt;loadISC&gt;, you'll need to merge the &lt;jsp-config&gt;
// section from web.xml.  Also there are some mandatory and optional servlets and filters to
// merge - see below.
// <li> <b>Power and Enterprise Editions only</b>.  Copy the shared/ds/batchUpload.ds.xml file
// to the same location in your target webapp directory.  This file is a utility DataSource
// that is used to provide the initial upload functionality of the +link{BatchUploader}
// component - strictly speaking, you only need to perform this step if you intend to use that
// component.
// </ul>
// <p>
// See +link{servletDetails,Core and Optional SmartClient servlets} for details of additional
// changes you may need to make to your applications <code>web.xml</code> file.  See
// +link{group:javaModuleDependencies,Java Module Dependencies} for details of the .JAR files that
// comprise the SmartClient Server, and their dependencies on various third-party libraries.
// <p>
// <u><b>Multiple Applications / WARs</b></u>
// <p>
// To integrate the server portion of SmartClient, you need to follow the steps below for each
// application (WAR) that uses SmartClient.  Note that, if installing into an environment that
// uses multiple WARs, installation of SmartClient JARs into a directory shared by multiple
// applications is not supported.  Installation of a separate WAR with client-side SmartClient
// modules for maintaining cache coherence across applications using the same version of ISC is
// supported - contact Isomorphic support for more details on how to set that up.
// <p>
// <u><b>Troubleshooting</b></u>
// <p>
// This section covers some common problems with possible solutions.  You may also need to refer to the
// documentation for your specific application server, web server, or database. If you experience any problems
// installing and configuring SmartClient in your environment, please post on the
// <a href="http://forums.smartclient.com/" target='_blank'>SmartClient forums</a> for
// assistance.
// <p>
// <table width="90%" class="normal" align="center" border="1" cellpadding="5">
// <tr bgcolor="#808080">
//     <td width="30%"><b>Problem</b></td>
//     <td width="30%"><b>Possible Causes</b></td>
//     <td width="40%"><b>Solution</b></td>
// </tr><tr>
//     <td rowspan='3'>Browser displays a generic "page cannot be displayed" or "unable to locate the server" message.</td>
//     <td>Servlet engine not started.</td>
//     <td>Start your application server.</td>
// </tr><tr>
//     <td>Missing/incorrect port for servlet engine in URL.</td>
//     <td>Check the startup messages, logs, or documentation for the servlet engine to determine what port it is using.</td>
// </tr><tr>
//     <td>Host name is incorrect.</td>
//     <td>Check whether other pages on the host can be accessed.
//         Try the base URL http://[host name]:[port number] to see whether the servlet engine or webserver is functioning.</td>
// </tr><tr>
//     <td>Browser displays a 404 or other page/file not found error.</td>
//     <td>Incorrect URL.</td>
//     <td>Check for errors in the URL, including capitalization.</td>
// </tr><tr>
//     <td>Server error: taglib not defined</td>
//     <td>Missing iscTaglib.xml or iscTaglib.xml not referenced in web.xml</td>
//     <td>Copy WEB-INF/iscTaglib.xml from smartclientRuntime to your deployment WEB-INF directory
//     and make sure that you have merged the &lt;jsp-config&gt; section from the
//     smartclientRuntime web.xml</td>
// </tr><tr>
//  <td>ClassNotFound or other Java Exceptions in the server log.</td>
//  <td>Missing JAR files</td>
//  <td>Verify every required .jar has been copied into the WEB-INF/lib directory of your
//  deployment.  Use +link{group:javaModuleDependencies,these docs} to double-check.  If in
//  doubt, copy every available .jar, verify this is working, then trim off .jars you are
//  definitely not using.</td>
// </tr><tr>
//  <td>"isc" is not defined JS error</td>
//  <td>Incorrect URLs to SmartClient modules</td>
//  <td>Use View Source to look at SCRIPT includes (e.g. for ISC_Core.js), try those URLs
//  directly in the browser to verify the files are correctly deployed</td>
// </tr>
// </table>
// <P>
// <u><b>Caching Considerations</b></u>
// <p>
// When upgrading from one SmartClient release to the next, you want to make sure that the user
// picks up the new version on next access, but you also want to keep the ISC modules cacheable
// so they're not refetched on every access.
// <p>
// SmartClient deals with this problem by appending a version string as a query parameter to
// each module load directive.  This is done by the &lt;isomorphic:loadISC&gt; and
// &lt;isomorphic:loadModules&gt; tags automatically.  As long as you make sure that the file
// that contains these tags is non-cacheable, you will get the desired behavior.
// <p>
// <u><b>Supported J2SE Containers</b></u>
// <p>
// Below is the list of J2SE containers that have been tested to be compatible with this
// version of SmartClient.  Installation in these containers is supported for deployment by
// Isomorphic.  If your application server is not on this list, please contact us at the
// <a href="http://forums.smartclient.com" target='_blank'>SmartClient forums</a> to see if we can support
// your deployment.  In general, the Java portion of ISC should work on servlet containers that
// comply with servlet specification version 2.3 and up and utilize a JVM no older than version
// 1.4.
// <p>
// Supported J2SE Containers:
// <p>
// <table class='normal'>
// <tr><td width=40></td><td width=400><i>Apache Tomcat 4.0.x, 4.1.x, 5.0.x, 5.5x, 6.0.x, 7.0.x, 8.0x</i></td><td></td></tr>
// <tr><td></td><td><i>Apache Geronimo 1.x, 2.x, 3.x</i></td><td></td></tr>
// <tr><td></td><td><i>Oracle WebLogic 6.x, 7x, 8.x, 9.x, 10.x, 11gR1 PSx, 12c Release x</i></td><td></td></tr>
// <tr><td></td><td><i>Caucho Resin 2.1.x, 3.0.x, 3.1.x, 4.x</i></td><td></td></tr>
// <tr><td></td><td><i>IBM WebSphere 5.x, 6.x, 7.x, 8.x</i></td><td></td></tr>
// <tr><td></td><td><i>IBM WebSphere Community Edition 1.x, 2.x, 3.x</i></td><td></td></tr>
// <tr><td></td><td><i>JBoss 3.2.x, 4.0.x, 4.2.x, 5.x, 7.x, 8.x</i></td><td></td></tr>
// <tr><td></td><td><i>Mortbay Jetty 4.x, 5.x, 6.x, 7.x, 8.x, 9.0.x</i></td><td></td></tr>
// <tr><td></td><td><i>Oracle Containers for J2EE (OC4J) 9.x, 10.x, 11.x</i></td><td></td></tr>
// <tr><td></td><td><i>Oracle Application Server 10g 9.x, 10.x; 11g</i></td><td></td></tr>
// <tr><td></td><td><i>Sun Application Server 8.x, 9.x</i></td><td></td></tr>
// </table>
//
// @treeLocation Concepts
// @title Deploying SmartClient
// @visibility external
//<



//> @groupDef sgwtEESetup
//
// <h3>Creating a new project from scratch</h3>
// <P>
// If you are starting a new project from scratch, we recommend starting with one of the sample
// projects included with SmartGWT EE under the "samples" directory.  Use the project that most
// closely matches your integration strategy and follow the instructions in the "readme" file
// within the sample directory.
// <P>
// <h3>Adding SmartGWT EE to an existing project</h3>
// <P>
// <B>Important:</b> If you have created a project using the GWT Plugin for Eclipse, a Maven archetype, or
// similar tool, first get rid of unused resources typically included in such "starter projects":
// <ul>
//   <li> from your [i]moduleName[/i].gwt.xml file, remove imports of any GWT themes, leaving
//        only the import of com.google.gwt.user.User.  Proper imports to add for SmartGWT
//        Pro/EE are shown below.
//   <li> get rid of any sample servlets or GWT-RPC services (delete both web.xml entries and
//        server-side source)
// </ul>
// <P>
// Now add SmartGWT EE: all of these steps are required.  <b>Do not skip steps or omit
// resources you think you may not need</b>.  Get a working project first, then think about
// trimming resources, as needed, once you understand the system.
// <P>
// In the steps below, copying from eg "war/WEB-INF/..." means copying from the "war"
// subdirectory of whichever sample project is closest to your desired integration strategy.
// <P>
// <ol>
// <li>Unless you're <a href="http://github.smartclient.com/isc-maven-plugin/">using Maven</a>
//     to manage dependencies, you'll need to add all jars from the SmartGWT Pro/EE
//     distribution (under smartgwtee-<i>version</i>/lib)
//     to your CLASSPATH.  You can copy the '.classpath' file from the appropriate sample
//     project for a working set of includes (it's in the same directory as the <i>war/</i>
//     folder). There are both client- and server-side jars, both kinds go in the classpath.
//     For server-side .jars, dependencies are documented +link{group:javaModuleDependencies,here}.
// <li>update your <i>modulename</i>.gwt.xml file with these imports:
//    <pre>
//    &lt;inherits name="com.smartgwtee.SmartGwtEE"/&gt;
//    &lt;inherits name="com.smartgwtee.tools.Tools"/&gt;
//    </pre>
//    <b>NOTE:</b> if you were previously using SmartGWT LGPL, <b>remove</b> the
//    &lt;inherit&gt; of 'com.smartgwt.SmartGWT' and ensure the &lt;inherit&gt; of
//    'com.smartgwt.tools.SmartGwtTools' appears <b>before</b> the imports above.  Also ensure
//    that all SmartGWT-related inherits appear *before* your &lt;entry-point&gt; declaration.
// <li> if you are using Optional Modules such as Analytics or RealtimeMessaging, see
//      +link{group:loadingOptionalModules} for additional inherits
// <li> Copy log4j.isc.config.xml across to the "src" dir of your project (it's
//      placed in the "src" dir as a means of getting it into the CLASSPATH).  This file is
//      either in the "src/" dir of a given sample project or in war/WEB-INF/classes.  This
//      enables default log4 categories for server-side logs appropriate for development.
// <li> Copy +link{group:server_properties,server.properties} across to the "src" dir of your project.
//      This file is either in the "src/" dir of a given sample project or in war/WEB-INF/classes.
//      This contains miscellaneous server settings - see the file itself for documentation.
//    <ul>
//       <li><b>server.properties contains server-side paths that contain the GWT module name</b>.
//           Replace the module name from the sample project (eg "builtinds") with the module
//           name of your actual GWT project
//       <li> if you're using SQLDataSource, enter your JDBC settings (start with a
//            server.properties from a project that uses a database to see the required format for
//            settings)
//    </ul>
// <li> If you plan to use JPA, copy src/META-INF/persistence.xml into your src/META-INF directory.
//      This file will be copied into war/WEB-INF/classes/META-INF directory during build process.
//      Make sure you specify your entity classes in persistence.xml. If you change persistence unit
//      name you have to update "jpa.persistenceUnitName" property in
//      +link{group:server_properties,server.properties} file. More
//      information on JPA configuration can be found in +link{group:jpaIntegration}.
// <li> merge web.xml, starting with the web.xml from the sample project that is closest to
//      your target application (under war/WEB-INF in the sample project).  The list of
//      servlets and what they do is documented +link{group:servletDetails,here}.  See the
//      server-side JavaDoc for further details on servlet APIs and override points.
// <li> Copy war/WEB-INF/iscTaglib.xml to your war/WEB-INF/iscTaglib.xml.  This is required for
//      JSP tags to work and used by tools such as Visual Builder.  Be sure you have also
//      copied the web.xml section that enables the taglib (&lt;jsp-config&gt; element)
// <li> (SmartGWT Power or Enterprise only) If using the BatchUpload functionality, copy
//      war/ds/batchUpload.ds.xml from the Showcase sample project to the shared.datasources
//      directory you configured in +link{group:server_properties,server.properties}.
// </ol>
// <p>
// <u><b>Troubleshooting</b></u>
// <table width="90%" class="normal" align="center" border="1" cellpadding="5">
// <tr bgcolor="#808080">
//     <td width="30%"><b>Problem</b></td>
//     <td width="30%"><b>Possible Causes</b></td>
//     <td width="40%"><b>Solution</b></td>
// </tr><tr>
//  <td>ClassNotFound or other Java Exceptions in the server log.</td>
//  <td>Missing JAR files</td>
//  <td>Verify every .jar from the smartgwtee-<i>version</i>/lib directory has been added to your CLASSPATH.
//  Although you might later be able to remove some .jars, for initial installation testing,
//  copy every .jar</td>
// </tr><tr>
//     <td>Client-side error about attempting to use "iscServer" request without server installed</td>
//     <td>Wrong imports in <i>moduleName</i>.gwt.xml</td>
//     <td>See correct imports above, note <b>remove</b> com.smartgwt.SmartGWT import and be sure
//      imports are in the correct order</td>
// </tr><tr>
//     <td>Client-side error about "$debox is not defined"</td>
//     <td>Wrong order of imports in <i>moduleName</i>.gwt.xml</td>
//     <td>Make sure all SmartGWT-related imports appear before &lt;entry-point&gt; in your
//      .gwt.xml file</td>
// </tr><tr>
//     <td>Missing images or failure to load page</td>
//     <td>Didn't set isomorphicDir in .html bootstrap file</td>
//     <td>See step above for setting isomorphicDir</td>
// </tr><tr>
//     <td>Server error: "adminConsole" app not found when launching tools such as Visual Builder</td>
//     <td>Bad filesystem paths configured in +link{group:server_properties,server.properties}</td>
//     <td>Correct paths in +link{group:server_properties,server.properties}.
//      <b>NOTE</b> the samples include the GWT module
//      name in some settings, search and replace this value with your GWT module name</td>
// </tr><tr>
//     <td>Server error: Can't find DataSource with ID <i>yourDataSourceID</i></td>
//     <td>Bad filesystem paths in +link{group:server_properties,server.properties} or
//      bad DataSource .ds.xml file</td>
//     <td>Correct paths in +link{group:server_properties,server.properties} - search for
//      "project.datasources".  Also check that
//      the DataSource ID you are using matches the "ID" attribute in the .ds.xml file (NOTE:
//      ID attribute is uppercase "ID" not "id") and that the file is named
//      <i>dataSourceId</i>.ds.xml.  DataSource IDs are <b>case sensitive</b>, including the
//      file name.  For more in-depth troubleshooting steps, see
//      +link{http://forums.smartclient.com/showthread.php?t=8159#aDSLoad,this FAQ answer}.</td>
// </tr><tr>
//     <td>Server error: taglib not defined</td>
//     <td>Missing iscTaglib.xml or iscTaglib.xml not referenced in web.xml</td>
//     <td>See step above about iscTaglib.xml and web.xml entry</td>
// </tr>
// </table>
// <P>
// For further troubleshooting steps, see
// +externalLink{http://forums.smartclient.com/showthread.php?t=8159,the SmartGWT FAQ} and, if
// still stuck, try posting in the Forums. <B>NOTE:</B> gather all the information indicated in
// the FAQ before posting.
// <p>
// <b>NOTE:</b>  It is possible to create a server-only SmartGWT EE project that will run
// standalone (ie, not inside a servlet container or application server).  Please see
// +link{groupDef:standaloneDataSourceUsage,Standalone DataSource Usage} for details.
//
// @title Smart GWT Pro/EE Project Setup & Deployment
// @visibility sgwt
//<

//> @groupDef server_properties
//
// The <code>server.properties</code> file is a configuration file read by the SmartClient
// server. It contains miscellaneous server settings - see the file itself for documentation
// and default or sample settings.
// <P>
// Note that this is a standard Java Properties file, except it allows variable substitution
// from other properties defined earlier in the file.
// <P>
// It's loaded from the <code>CLASSPATH</code>, so it can be anywhere in the
// <code>CLASSPATH</code>, but is typically either in the projects java "src" directory or
// in <code>WEB-INF/classes</code>.
// <P>
// Server side code can access and modify the properties specified in this file via the
// <code>com.isomorphic.base.Config</code> class
//
// @visibility external
// @title <code>server.properties</code> file
// @treeLocation Concepts/Deploying SmartClient
//<

//> @groupDef servletDetails
//
// The following is a description of the servlets and filters you'll find in the web.xml file
// contained in the smartclientRuntime and what they do:
// <p>
// <i>Core Functionality</i>
// <ul>
// <li>Init servlet - see the article on +link{group:serverInit,SmartClient Server Initialization</li>
// <li>IDACall servlet - <b>required</b> for +link{group:dmiOverview,DMI}, built-in RPC operations and built-in DataSource
// operations to work.  All databound examples in the SDK use this servlet.  If you're planning on
// using a custom actionURL for all your RPC requests, then you don't need this servlet.
// <li>FileDownload servlet - required for serving the Isomorphic framework code compressed and with
// caching headers as well as for serving skin images with caching headers.  It is highly
// recommended that you use this for production but is not required.
// <li>PreCache servlet - loads resources into memory on container startup.  Not required, but if you
// exclude this servlet there may be a slow response to the very first request.
// <li>jsp-config section - the iscTaglib registration block is required to use
// &lt;isomorphic&gt; tags, and the *.isc and *.rpc mappings.  These are optional, if you want
// to use these as handy development tools.
// </ul>
// <p>
// <i>Optional Functionality</i>
// <ul>
// <li>RESTHandler servlet - handles SmartClient Server DataSource operations issued by
// REST clients: it's like IDACall, but for the REST protocol.  Typically,  the clients
// of this servlet would not be ordinary SmartClient/SmartGWT applications (though they
// could be), but other client technologies that need to access SmartClient DataSource
// operations as reusable services.  If you do not plan to connect to the server using the
// REST protocol, then you don't need this servlet.
// <li>AxisServlet - exposes all DataSource operations via a WSDL service described by
// SmartClientOperations.wsdl.  This is effectively the same as the RESTHandler servlet, but
// for SOAP clients.  If you do not plan to connect to the server using webservice protocols,
// then you don't need this servlet.
// <li>HttpProxy - used by the RPCManager when sending AJAX RPCs to a server other than the
// server that serves the main application page.  You need to install this servlet if, for
// example, your application will be querying web services exposed by servers other than the
// server that is serving the rest of the application.  See the javadoc for this servlet for
// various configuration options, such as how to restrict the URLs that are allowed to be
// proxied.
// <li>MessagingServlet - used by the realtime messaging system.  If you're planning
// on using this subsystem, you'll need this servlet.
// <li>CompressionFilter - required if you want to use dynamic compression of html and js
// files.
// <li>JSSyntaxScannerFilter - development tool that looks for trailing commas in JS
// source (scans html files for &lt;script&gt; tags and scans .js files in their entirety).
// This is a useful development tool, but should not be included in production.
// <li>NoCacheFilter - development tool that makes any content it intercepts non-cacheable in
// order to ensure developers are looking at the latest version of a file when modifying
// examples.  Not for production use.
// <li>DataSourceLoader - a servlet that returns the definition of one or more DataSources in
// JavaScript notation.  This servlet is provided as an alternative to using the
// <code>&lt;isomorphic:loadDS&gt;</code> JSP tag, and is particularly suitable in environments
// where JSP tags can't be used for some reason (such as with SmartGWT).  See
// +link{group:dataSourceDeclaration,Creating DataSources} for more details.
// <li>ScreenLoaderServlet - a servlet that returns the definition of one or more screens in
// JavaScript notation.</li>
// </ul>
// Note that not all of the servlets and filters listed under <i>Optional Functionality</i>
// above are present in the web.xml that ships with the smartclientRuntime - if you need to use
// any of these, copy their configuration from the web.xml available under the WEB-INF
// directory of smartclientSDK.  Other servlets, filters and configuration files from the
// smartclientSDK should not be copied to your deployment, simply because the SDK includes many
// developer tools that are not extensively audited from a security standpoint.
//
// @treeLocation Concepts/Deploying SmartClient
// @title The Core and Optional SmartClient servlets
// @visibility external
//<


//> @groupDef serverInit
//
// The SmartClient Server framework must be initialized from its config files at startup
// time, before user code invokes <em>any</em> framework functionality.  If you are running
// standalone (ie, outside of a servlet container), you should call the static utility method
// <code>ISCInit.go()</code> early in your bootstrap code (eg, from the top of your
// <code>main()</code> method).
// <p>
// If you are running inside a servlet engine, there are two ways to initialize the framework:
// <ul>
// <li>Install <code>InitListener</code>, which is a <code>ServletContextListener</code>:<br>
// <pre>   &lt;listener&gt;
//        &lt;listener-class&gt;com.isomorphic.base.InitListener&lt;/listener-class&gt;
//    &lt;/listener&gt;</pre></li>
// <li>Install <code>Init</code>, which is a <code>Servlet</code>, to load at startup:<br>
// <pre>    &lt;servlet&gt;
//        &lt;servlet-name&gt;Init&lt;/servlet-name&gt;
//        &lt;servlet-class&gt;com.isomorphic.base.Init&lt;/servlet-class&gt;
//        &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
//    &lt;/servlet&gt;</pre></li>
// </ul>
// As shipped, the framework's <code>web.xml</code> file loads both of these classes, to ensure
// the best chance of correct and early initialization.  The recommended approach is to use
// <code>InitListener</code> if your servlet container implements the Servlet 2.4 API or
// greater, because <code>ServletContextListener</code>s are guaranteed to run before any
// servlets or filters are instantiated.  <code>ServletContextListener</code>s are available
// in Servlet API 2.3, but the requirement that they run before any filter or servlet is
// initialized was not added until 2.4.  Some 2.3 servlet engines do enforce this behavior
// even though it is not part of the spec; experimentation with your servlet container of
// choice will be required if it only implements 2.3.
// <p>
// Note that it does no harm to leave the <code>web.xml</code> file with both <code>Init</code>
// and <code>InitListener</code> in place, because the framework simply ignores any request
// to initialize once initialization has taken place.
// <p>
// <h4>Interaction with Spring initialization</h4>
// The Spring framework attempts to initialize itself in similar ways to the SmartClient
// framework, and similarly it wants its own initialization to be the very first thing that
// happens.  This can cause problems in certain cases.  For example, one known problem occurs
// when you define DataSource instances as Spring beans, and Spring gets to initialize itself
// first: it attempts to instantiate a instance of the bean, which ultimately calls into
// our framework code before the framework has been initialized.
// <p>
// The solution to this is to use the <code>InitListener</code>, and to ensure that it is
// declared in your <code>web.xml</code> file before the Spring
// <code>ContextLoaderListener</code>.  The Servlet 2.3 spec states (SRV.10.3.3):<p>
// <em>&nbsp;&nbsp;"During Web application execution, listeners are invoked in the order of their
// registration."</em>
// <p>
// Therefore, declaring the SmartClient listener before the Spring one ensures that
// things run in the correct order in Servlet 2.4-compatible containers, and in those Servlet
// 2.3 containers that enforce the rule of running listeners before initializing filters or
// servlets.
//
// @treeLocation Concepts/Deploying SmartClient
// @title Server Framework Initialization
// @visibility external
//<

//> @groupDef javaModuleDependencies
// Maven users should generally refer to the POMs bundled with the SDK, and installed for them
// by the official <a href="http://github.smartclient.com/isc-maven-plugin/">Isomorphic plugin for Maven</a>.
//
// For others, the following is a description of what functionality is contained in each SmartClient JAR
// file, and which other SmartClient JARs and third party libraries are required to enable that
// functionality.  Note that actual filenames in WEB-INF/lib will typically contain the version
// number of the library, whereas the library names below simply list the base name.
// <P>
// All of the libraries that SmartClient depends upon have either an Apache 1.0 or
// Apache 2.0 license, with the following exceptions:<br>
// 1. Optional Hibernate support requires LGPL-licensed .jars from Hibernate<br>
// 2. Optional bean annotation support requires an implementation of javax.persistence.jar.
// This is available from several providers - for example, SpringSource, Hibernate or the
// Glassfish project.  The licensing of this library varies by provider.<br>
// 3. Optional support for Java as a JSR223 server-side scripting language is provided by
// software developed by Isomorphic, but based on the "java-engine" package, originally
// developed by Sun.  See +link{sunNotice}.
// <P>
// If you are using GWT, GWT itself also has an Apache 2.0 license, however tools and
// test environments that you may use during development have different license (such as
// Eclipse - Eclipse Public License).  Also, specific GWT widgets, not required by Smart GWT,
// have licenses different from core GWT (such as JFreeChart's LGPL license).  See
// +externalLink{http://code.google.com/webtoolkit/terms.html,Google's summary of terms} for
// details.
// <ul>
// <li><b>isomorphic_core_rpc</b>: This is the core SmartClient module.  It provides the RPC, DMI,
// and DataSource support.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-cli<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-lang<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-collections<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-pool<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-codec<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isc-jakarta-oro<br>
// &nbsp;&nbsp;&nbsp;&nbsp;log4j<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-jxpath<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-httpclient<br>
// &nbsp;&nbsp;&nbsp;&nbsp;velocity<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-fileupload<br>
// &nbsp;&nbsp;<u>Optionally Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_js_parser - if you're using the built-in support for REST via the RESTHandler servlet with JSON payloads<br>
// &nbsp;&nbsp;&nbsp;&nbsp;xercesImpl - if you're using JDK &lt; 1.5<br>
// &nbsp;&nbsp;&nbsp;&nbsp;xml-apis - if you're using JDK &lt; 1.5<br>
// &nbsp;&nbsp;&nbsp;&nbsp;mail - if you plan to use the Mail messaging feature<br>
// &nbsp;&nbsp;&nbsp;&nbsp;activation - if you plan to use the Mail messaging feature and you
// are using a JDK &lt; 1.6<br><br>
// &nbsp;&nbsp;&nbsp;&nbsp;javax.persistence - if you plan to use the metadata-from-annotations
// feature.  Note that if you are using JPA, or a recent version of Hibernate, then you are
// probably already using this library.<br><br>
// &nbsp;&nbsp;&nbsp;&nbsp;poi - if you plan to export datasets in Microsoft Excel 97 (xls)
// or 2007 (xlsx) formats.  Additionally, if you plan to export data in Excel 2007 (xlsx)
// format, you will need the following libraries:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;poi-ooxml, poi-ooxml-schemas, xbean, jsr173_1.0_api<br><br>
// &nbsp;&nbsp;&nbsp;&nbsp;PDF Export - Additional .jars are required if PDF Export is to be used. These .jars are:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;core-renderer, iText-2.0.8, jtidy-r938<br>
// &nbsp;&nbsp;&nbsp;&nbsp;Image Export and IE6-8 DrawPane PDF Export - These libraries are required to use
// +link{RPCManager.exportImage()}, or when using +link{RPCManager.exportContent()} to export a DrawPane or FacetChart
// only in IE8 or earlier:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;batik-anim, batik-awt-util, batik-bridge, batik-css,
// batik-dom, batik-ext, batik-gvt, batik-parser, batik-script, batik-svg-dom, batik-util, batik-xml,
// xml-apis-ext<br>
// &nbsp;&nbsp;&nbsp;&nbsp;The above libraries are also required if a DrawPane
// has a DrawImage which loads a cross-domain image.<br><br>
//
// &nbsp;&nbsp;&nbsp;&nbsp;<b>NOTE:</b> Between iText 2.0.x and iText 2.1.x there is a binary
// (but not source) incompatibility that causes a server-side <code>NoSuchMethodError</code> when exporting,
// e.g., charts in Internet Explorer. This is a +externalLink{http://code.google.com/p/flying-saucer/issues/detail?id=126,known issue}
// with the Flying Saucer product that is fixed by using core-renderer-R8-isomorphic.jar and iText-2.1.7.jar in the
// lib-iTextAlternate/ directory instead of core-renderer.jar and iText-2.0.8.jar in the lib/ directory.
// To use iText 2.1.7 with the Server Framework, exclude lib/core-renderer.jar and lib/iText-2.0.8.jar
// from the application's classpath and instead add lib-iTextAlternate/core-renderer-R8-isomorphic.jar and
// lib-iTextAlternate/iText-2.1.7.jar.<br><br>
//
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_jpa and its dependencies - if you plan to use BatchDS Generator even if you are
// not using JPA, although the generated DataSources will not require JPA at runtime if you are not using JPA.<br><br>
//
// &nbsp;&nbsp;&nbsp;&nbsp;groovy - if you plan to use Groovy with the +link{group:serverScript} feature.
// </li>
// <p>
// <li><b>isomorphic_contentexport.jar</b>: Optional support for PDF Export requires Flying Saucer, iText and jTidy, introducing:<br>
// &nbsp;&nbsp;- Flying Saucer: core-renderer.jar: +externalLink{http://www.gnu.org/licenses/lgpl-2.1.html,LGPL license}<br>
// &nbsp;&nbsp;- iText: iText-2.0.8.jar: +externalLink{http://www.mozilla.org/MPL/1.1/,Mozilla Public License Version 1.1}<br>
// &nbsp;&nbsp;- jTidy: jtidy-r938.jar: +externalLink{http://jtidy.sourceforge.net/license.html,MIT license}<br><br>
// Additionally, to be able to export DrawPanes and FacetCharts in IE6-8,
// or to export FacetChart zoom charts (see +link{FacetChart.printZoomChart}),
// the following libraries are needed:<br>
// &nbsp;&nbsp;- Batik: batik-anim, batik-awt-util, batik-bridge, batik-css, batik-dom, batik-ext,
// batik-gvt, batik-parser, batik-script, batik-svg-dom, batik-util, batik-xml:
// +externalLink{http://xmlgraphics.apache.org/batik/license.html,Apache License&#44; Version 2.0}<br>
// &nbsp;&nbsp;- xml-commons External Components: xml-apis-ext:
// +externalLink{http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231,W3C Software Notice and License}
// </li>
// <p>
// <li><b>isomorphic_web_services</b>: Web services examples only.  Contains code backing the
// SmartClientOperations.wsdl example.  Do not deploy in production.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;axis<br>
// &nbsp;&nbsp;&nbsp;&nbsp;axis-schema<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-discovery<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-logging<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jaxrpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;saaj<br>
// &nbsp;&nbsp;&nbsp;&nbsp;wsdl4j<br>
// </li>
// <li><b>isomorphic_tools</b>: Contains back-end logic for the "Admin Console" tool visible in
// the Developer Console, and also standalone from the SDK home page.  Also contains the the
// various data importers and exporters, and the server-side components of the BatchUploader.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_sql<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_hibernate<br>
// </li>
// <li><b>isomorphic_embedded_tomcat</b>: This contains the bootstrap class for the Embedded
// Tomcat engine shipped with the SmartClient SDK.  It is not intended to be used in any way
// other than via the start_embedded_tomcat.bat|sh|command scripts in the webRoot of the
// SDK.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;All JARs under WEB-INF/embeddedTomcat/lib<br>
// </li>
// <li><b>isomorphic_spring</b>: Required for +link{group:dmiOverview,DMI} dispatches to Spring beans (via
// +link{serverObject.lookupStyle} : "spring").<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;spring<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-logging<br>
// </li>
// <li><b>isomorphic_examples</b>: Contains code for various SmartClient examples shown in the
// SDK.  Do not deploy to production.  The source code for the compiled classes in this module
// are present in the SDK in various locations and typically linked to directly from the
// examples that use them.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_sql<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_struts<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_spring<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_hibernate<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-logging<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jstl<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jakarta-taglib-standard<br>
// </li>
// <li><b>isomorphic_realtime_messaging</b>: Server support required for the SmartClient
// Realtime Messaging Module.  Install this if you're using this 'push' technology.  For more
// information, see +link{group:messaging,Messaging}:<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// </li>
// <li><b>isomorphic_hibernate</b>: Contains support for Hibernate DataSources as described
// here: +link{group:hibernateIntegration}.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_sql<br>
// &nbsp;&nbsp;&nbsp;&nbsp;hibernate3<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-logging<br>
// &nbsp;&nbsp;&nbsp;&nbsp;dom4j<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jdbc2_0-stdext<br>
// &nbsp;&nbsp;&nbsp;&nbsp;asm<br>
// &nbsp;&nbsp;&nbsp;&nbsp;asm-attrs<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jta<br>
// &nbsp;&nbsp;&nbsp;&nbsp;cglib<br>
// &nbsp;&nbsp;&nbsp;&nbsp;antlr<br>
// &nbsp;&nbsp;<u>Optionally Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;hsqldb - not required for linking, but needed for actually connecting
// to databases of this type, and hsqldb is what's used in the SDK examples.
// Note that for production use you will need a JDBC driver for your database type.<br>
// </li>
// <li><b>isomorphic_jpa</b>: Contains support for JPA DataSources as described +link{group:jpaIntegration,here}.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;Hibernate and its dependencies or any other JPA persistence provider<br>
// </li>
// <li><b>isomorphic_sql</b>: The SmartClient SQLDataSource.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-dbcp<br>
// &nbsp;&nbsp;&nbsp;&nbsp;hsqldb<br>
// </li>
// <li><b>isomorphic_autotest</b>: Support for
// +link{group:automatedTesting,automated testing and Continuous Integration}<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;selenium-server<br>
// &nbsp;&nbsp;<em>The <code>selenium-server.jar</code> file provided with SmartClient is a
// trimmed version that does not include all of the libraries used by Selenium itself.
// Therefore the following libraries are also needed:</em><br>
// &nbsp;&nbsp;&nbsp;&nbsp;backport-util-concurrent<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-configuration<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-exec<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-io<br>
// &nbsp;&nbsp;&nbsp;&nbsp;guava<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jna (if testing with IE on Windows environments)<br>
// &nbsp;&nbsp;&nbsp;&nbsp;jna-platform (if testing with IE on Windows environments)<br>
// &nbsp;&nbsp;<em><u>NOTE:</u> <code>servlet-api.jar</code> is also required if you intend to
// run Selenium RC from a standalone process (ie, from a normal Java program, not a webapp).
// However, it should <u>not</u> be deployed to a servlet container such as Tomcat or Jetty.
// The best case is that the file will be unused and a source of confusion for anybody looking
// at the webapp's library usage; the worst case is that it will conflict with the container's
// own implementation of the Servlets API.</em>
// </li>
// <li><b>isomorphic_js_parser</b>: A parser capable of reading a JSON byte stream and creating
// an in-memory Java object structure to match.  Used by any mechanism that relies on
// JSON-style configuration.  Examples include FileAssembly definitions in JSON format, any use
// of the rulesFile with a URIRegexFilter (Java Servlet) or subclass.  Otherwise, generally not
// required except as documented in another module (e.g., isomorphic_core_rpc).
// <br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// </li>
// <li><b>isomorphic_compression</b>: This is a part of the Network Performance Module.  The
// isomorphic_compression module is required for dynamic and static compression of various
// assets delivered to the browser.  For more information, see:
// +link{group:compression}.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// </li>
// <li><b>isomorphic_assembly</b>: This is part of the Network Performance Module.  The
// isomorphic_assembly module is required for file assembly and stripping.  For more
// information, see: +link{group:fileAssembly}.<br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// </li>
// <li><b>isomorphic_struts</b>: Contains the ValidationAction and ValidationFailureAction
// classes that implement RPC-based validation of DynamicForms using the Struts
// ValidatorPlugIn.  If you're not using Struts or if you don't need this particular feature of
// SmartClient, you do not need this module or its dependencies (also see the important note
// below).  An example of this style of validation is available here:
// +externalLink{/examples/struts/forms/welcome.do} - read the info on this page, and follow
// the "Dynamic Form (With RPC-based Validation)" Link for the actual example.<br><br>
// <b>NOTE:</b> This support is for Struts 1.0 only, and is only intended to be used in certain
// edge cases of incremental migration to SmartClient.  You should only use it if directed to
// do so by Isomorphic Support.<br><br>
// &nbsp;&nbsp;<u>Requires</u>:<br>
// &nbsp;&nbsp;&nbsp;&nbsp;isomorphic_core_rpc<br>
// &nbsp;&nbsp;&nbsp;&nbsp;struts<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-digester<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-beanutils<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-logging<br>
// &nbsp;&nbsp;&nbsp;&nbsp;commons-validator<br>
// </li>
// </ul>
//
// @treeLocation Concepts/Deploying SmartClient
// @title Java Module Dependencies
// @visibility external
//<


//> @groupDef iscServer
//
// The SmartClient Server is a set of Java libraries, servlets and tools that provide the key
// server-side components needed to build a complete application in the modern web
// architecture.
// <P>
// The SmartClient Server can be +link{group:iscInstall,integrated} into any pre-existing Java
// application, and is designed to rapidly connect SmartClient visual components to
// pre-existing Java business logic or persistence engines.  SmartClient's Ajax request
// processing facilities can be easily integrated into Struts Actions,
// +link{group:springIntegration,Spring controllers}, or custom servlets and JSPs.
// <P>
// Alternatively, the SmartClient Server provides a complete SQL and Hibernate-based
// persistence engine for new applications, with out-of-the-box servlets for processing Ajax
// data requests.
// <P>
// The SmartClient Server is optional, and SmartClient's client-side Ajax engine can be
// integrated with any server that provides HTTP access, using XML, JSON, SOAP or proprietary
// data protocols.  However any server in a modern web application will be required to provide
// most or all of the features of the SmartClient Server (described below), and the SmartClient
// Server represents a best-of-breed implementation of these facilities with a long history of
// high-volume production deployments.
// <P>
// <h4>Server enforcement of +link{class:Validator,Validators}</h4>
// <P>
// Data passed from the browser can be automatically validated by the SmartClient Server.
// In contrast, when using +link{group:clientDataIntegration,client-side integration},
// data arrives as HTTP params or XML messages, and you must parse values into the correct
// types (eg java.util.Date) and validate them, or use a server framework that does so.
// <P>
// <h4>High Speed Data Delivery / Data Compression</h4>
// <P>
// The SmartClient Server delivers data to and from the browser using a proprietary, maximally
// efficient protocol, providing simple Java APIs for sending and receiving data.
// <P>
// SmartClient's data protocol is:
// <ul>
// <li> automatically compressed: provides 6-8x improvement in bandwidth utilization
// <li> efficient on the server: high speed data serialization for any Java Object
// <li> efficient in the browser: faster than ordinary XML or JSON data delivery
// <li> minimal: facilities for +link{dataSource.dropExtraFields,trimming} and
// +link{dataSourceField.valueXPath,extracting} only the data you want the browser to see
// </ul>
// <P>
// <h4>Transparent upload support</h4>
// <P>
// SmartClient provides special client and server-side support for +link{group:upload,file
// upload}, which allows single and multiple-file HTTP uploads to be performed as a background
// Ajax request without reloading the page or launching sub-windows.
// <P>
// Uploaded files arrive at the SmartClient server as Java InputStreams accessible from the
// DSRequest object, and can optionally be automatically stored via SmartClient's SQL
// subsystem.
// <P>
// <h4>Transparent Queuing / "Batch" Operations</h4>
// <P>
// Any request transmitted to the SmartClient Server can be combined into a "queue" transmitted
// as a single HTTP request, with in-order execution for all queued operations.
// +link{RPCManager.startQueue,startQueue()} starts a queue and
// +link{RPCManager.sendQueue,sendQueue()} transmits it; queuing is transparent to the code
// that initiates the individual requests.  This enables:
// <ul>
// <li> re-use of data access operations across different screens
// <li> easy implementation of transaction boundaries
// <li> simplified saving and loading of screens with complex, master-detail views
// <li> guaranteed in-order processing of operations
// <li> more efficient network usage
// </ul>
// <P>
// <h4>Visual Builder</h4>
// <P>
// +link{group:visualBuilder,Visual Builder} is included with the SmartClient Server, and uses
// server features such as automatic SQL binding to provide a rapid prototyping environment.
// <P>
// <h4>Automatic Bi-directional Java &lt; - &gt; JavaScript serialization and translation</h4>
// <P>
// Provides a powerful, type-safe +link{rpcRequest.data,data transmission mechanism} for moving
// data between a Java server and the browser.
// <P>
// Any Java objects, including Java Beans, POJOs, Java Collections, XML DOMs and all Java
// primitives, with any level of nesting, can be automatically serialized and delivered as
// JavaScript Objects to the SmartClient client-side components.
// <P>
// JavaScript Objects existing in the browser can likewise be automatically transmitted to a
// Java Server and translated to Java Objects, with any level of nesting and automatic
// preservation of primitive types.
// <P>
// <h4>SQL and Hibernate connectors</h4>
// <P>
// DataSources of serverType:"sql" or serverType:"hibernate" can generate and execute queries
// against popular SQL engines or against the Hibernate ORM system, providing SmartClient's
// +link{DataBoundComponent}s with the four standard CRUD operations (create, retrieve, update,
// delete) without writing any server-side code.  For rapid prototyping, these DataSources can
// even generate SQL tables based on the DataSource declaration, using the
// +link{group:adminConsole} visual tool.
// <P>
// Server-side APIs allow server-side modification of the request before it is executed (for
// example, to enforce security) and post-processing of the request after execution (for
// example, to provide calculated values).
// <P>
// Both serverType:"sql" and serverType:"hibernate" support the field-operator-value queries
// that can be generated by using the +link{FilterBuilder} component (see
// +explorerExample{filterBuilderBracket,example}).
// <P>
// <h4>Rich, Standardized Request / Response protocol</h4>
// <P>
// The SmartClient Server provides a standardized request and response protocol designed for
// data-oriented "CRUD" operations (create, retrieve, update, delete).
// <P>
// This standardized protocol automatically handles +link{DSRequest,request metadata} (paging
// parameters, requested sort order, original values of data being modified) and
// +link{DSResponse,response metadata} (error handling, cache management, session expiration etc).
// <P>
// Similar to the impact of Struts on early web applications, this standardized protocol avoids
// developers in different groups inventing their own incompatible and redundant
// request/response protocols, and allows developers to more easily learn code they didn't
// author.
// <P>
// <h4>Bi-directional XPath binding to Java Objects</h4>
// <P>
// Most UI designs do not directly reflect the underlying Object model and so some degree of
// translation is necessary in order to populate UI components with data and apply user changes
// to the Java Object model.  This is often accomplished with brittle, difficult to understand
// data translation code sprinkled throughout the system, done in a different way for every
// screen or component.
// <P>
// SmartClient provides a standard, +link{dataSourceField.valueXPath,XPath-based approach} to
// adapting any Java-based Object model to the requirements of the UI design.  Data relevant to
// the application UI is centrally extracted in the server-side +link{DataSource} layer, so
// that all UI components have a consistent, unified view of the data model for both loading
// <b>and</b> saving data.
// <P>
// <h4>Broadest possible browser support</h4>
// <P>
// The SmartClient Server can compensate for facilities
// +link{group:platformDependencies,missing or disabled in certain browsers},
// including ActiveX being disabled in IE6 and missing XML support in some versions
// of Apple's Safari browser.
// <P>
// <h4>Transparent Proxying</h4>
// <P>
// +link{RPCManager.sendProxied,Proxying} allows SmartClient applications to access web
// services, RSS feeds, HTML content and other data services in a secure manner regardless of
// where they are located: across the enterprise or publicly available.
// <P>
// <h4>Optional +link{group:networkPerformance,Network Performance} Module</h4>
// <P>
// Provides:
// <ul>
// <li> compressed delivery of SmartClient runtime, application logic and other assets such as CSS
// <li> +link{FileLoader,background download} of SmartClient and other assets for zero
// user-perceived load time
// <li> on-the-fly stripping and combining of JavaScript (application code and data)
// <li> browser cache control
// </ul>
// <P>
// <h4>Optional Messaging Module (aka server push)</h4>
// <P>
// The +link{group:messaging,Messaging} module allows the server to "push" messages to the client, without
// client polling, for real-time monitoring/dashboarding applications.
//
// @title SmartClient Server Summary
// @treeLocation Java Server Reference
// @visibility external
//<


//> @groupDef jsfIntegration
//
// SmartClient can be used within JSF applications to add AJAX richness and interactivity.
// <P>
// Because JSF is a pre-AJAX architecture, the recommended approach in adding
// SmartClient to JSF applications is to create pages that use SmartClient components exclusively,
// so that older, server-based JSF components do not introduce full-page refreshes.
// <P>
// JSF pages that render components on the server access data via JSF Expression Language.
// SmartClient-based JSF pages can similarly load initial data by using JSTL, as shown in
// +externalLink{/examples/server_integration/#jstlList,this example}, where a ListGrid is
// populated by JSTL access to Java Beans stored in the JSP <code>pageContext</code>.
// <P>
// Once a SmartClient JSF page has loaded, SmartClient components will request data via background
// HTTP requests that load only data, not a complete page.  The
// +link{group:dmiOverview,Direct Method Invocation} system can be used to declaratively map SmartClient's
// background data requests directly to Java Methods. The SmartClient server automatically
// translates inbound request data into Java Objects that are passed to the method you specify,
// and the Java method return value is automatically translated into data for SmartClient
// components.
// <P>
// <h4>Incorporating server-side JSF components into a SmartClient JSF page</h4>
// <P>
// An +link{HTMLFlow} or +link{HTMLPane} component can be used to incorporate server-generated
// content within a SmartClient-based page.  With +link{htmlFlow.contentsType,contentsType} set to
// "page", the HTMLPane/Flow will act like a standalone page-within-a-page (via a
// SmartClient-managed HTML IFRAME element), allowing interactive server-side JSF components to
// participate normally, with limitations as discussed under the documentation for
// +link{htmlFlow.contentsType,contentsType}.
//
// @treeLocation Concepts
// @title Integration with JSF
// @visibility external
//<

//> @groupDef strutsIntegration
//
// <b>Overview of SmartClient integration with Struts and other popular server-side
// frameworks.</b>
// <p>
// Current and upcoming server-side frameworks solve a lot of problems endemic to the past
// generation of web applications.  Piles of JSPs and Servlets have been replaced by MVC and
// other paradigms that enhance developer productivity.  Unfortunately the end-user
// presentation remains vanilla HTML.  SmartClient solves this problem by providing rich
// databindable components. SmartClient was designed to integrate cleanly with existing
// server-side frameworks.  Because SmartClient components only require an HTML context to
// render, they can be used with any server-side frameworks that use HTML for presentation.
// <p>
// SmartClient provides a rich UI by doing more work on the client (browser) rather than
// sending every user action to the server for re-rendering.  Instead of doing page transitions
// to redraw the whole page, SmartClient sends RPC (Remote Procedure Call) requests (or AJAX
// requests) to the server while the UI allows the user to continue interacting with the
// system.  Current server-side frameworks, on the other hand are typically designed around
// page transitions - for example in Struts user actions are typically mapped to URLs that
// dispatch through a central servlet and ultimately return new HTML to be rendered by the
// browser.  The problem with page transitions is that they destroy client-side state and
// introduce client-server latency (and generally use more bandwidth since HTML is sent over
// the wire rather than just data) - essentially destroying a large part of the Rich
// Internet Application (RIA) experience.
// <p>
// Fortunately, there's a way to get the best of both worlds - to leverage the power of your
// favorite server-side framework and combine it with the SmartClient UI.  There are several
// approaches to integrating SmartClient into an existing framework:
// <p>
// <u><b>Plug-replacing HTML components with SmartClient components</b></u>
// <p>
// SmartClient components can be instructed to draw at page load time using by specifying
// <code>position: "relative"</code> at construction time.  This enables you to replace any
// chunk of HTML with a SmartClient component - the new component simply inserts its HTML in
// the page flow during page load.  This is the easiest integration option - you get a better
// UI with minimal work.  The downside is that you don't get the full power of a rich client
// because most user actions will still trigger a page transition.
// <p>
// <u><b>Eliminating page transitions</b></u>
// <p>
// Most SmartClient components can accept new data (or even dynamically pre-fetch and expire
// data) without needing to be recreated.  For example - let's say you want to draw a grid on a
// page.  In a traditional server-side-rendered application the server would generate all of
// the html with "next 20 records" and "previous 20 records" buttons. When the user wants to
// see the next set of data, he clicks one of the buttons and the server replaces the entire
// page with a new grid that contains the next/previous 20 records.  In a SmartClient
// application, you would create a databound ListGrid.  Based on its configuration this grid
// will fetch the first N (say 20) records and display a scrollbar for the user to scroll
// through the data.  When the user scrolls off the last cached record the ListGrid
// automatically sends an RPC to the server asking for the next 20 records.  This RPC (fetch)
// is performed without destroying the page the user is currently looking at - it just happens
// seamlessly in the background.  If the user now scrolls back to the first 20 records -
// they're already cached in the grid, so no fetch is performed.  Of course, in a real world
// application, it's typical that a page has hundreds of components and in a server-side-only
// rendering all of them need to be rebuilt by the server and resent to the client when a piece
// of data in just one needs to be updated.  SmartClient components can intelligently update
// just their data without the need to redraw the whole page.
// <p>
// The plug-replacement strategy listed above gives us a SmartClient component in place of a
// raw HTML rendering.  Now we need to databind that component so that actions like scrolling a
// grid or validating a form don't cause a page transition.  To do this, you need to set up a
// Struts Action that will handle <b>all</b> SmartClient <code>RPCRequest</code>s and
// <code>DSRequest</code>s.  This is important, as requests need to be sent to the same URL
// to enable queuing to work.  In your Action class, you simply need to invoke
// <code>RPCManager.processRequest()</code> to hook straight into the normal
// <code>DSRequest</code> processing flow.
// <p>
// The SDK contains a simple example of doing form validation without incurring a page
// transition. These examples also show how to populate e.g. field names using the struts-bean
// taglib and how to set validation errors using the standard Struts Validation plugin.
// Point your browser to +externalLink{/examples/struts/forms} in the SmartClient SDK to take a
// look.
//
// @treeLocation Concepts
// @title Integration with Struts
// @visibility external
//<

//> @groupDef springIntegration
// <b>Overview</b>
// <P>
// The Spring framework has many different parts, from integration with Object Relational
// Mapping (ORM) and transaction management systems, to a Model View Controller (MVC)
// architecture.
// <P>
// If you are building a new application from scratch and/or you are trying to
// modernize the presentation layer of an existing application, most of Spring MVC is
// inapplicable in the +link{group:smartArchitecture,SmartClient architecture}.  Specifically,
// SmartClient renders <b>all</b> HTML on the client, and the server is responsible only for
// retrieving data and enforcing business rules.  This means that Spring's ModelAndView and all
// functionality related to retrieving and rendering Views is unnecessary in SmartClient.
// SmartClient only needs the Model, and provides methods to deliver that Model to SmartClient
// components (the server side method DSResponse.setData()).
// <P>
// However, Spring's DispatchServlet, Handler chain, and Controller architecture is applicable
// to SmartClient.  See "Using Spring Controllers" below.
// <P>
// <b>Existing Spring Application</b>
// <P>
// As discussed under the general +link{group:clientServerIntegration,server integration}
// topic, integrating SmartClient into your application involves finding a way to provide data
// that fulfills the +link{DSRequest,DataSource requests} sent by SmartClient components.
// <P>
// There are 2 approaches for integrating SmartClient into an existing Spring application:
// <ul>
// <li> <b>call Spring beans via SmartClient DMI or custom DataSources</b>
// <span style="color:red">[Recommended]</span>: use SmartClient Direct Method Invocation
// (DMI) to map +link{dsRequest,DataSource requests} to beans managed by Spring, via
// +link{serverObject.lookupStyle}:"spring".   Return data to the browser by either simply
// returning it from your method, or via creating a DSResponse and calling DSResponse.setData()
// (server-side method).  Or, use a similar approach based on custom DataSource implementations
// where the +link{dataSource.serverConstructor,serverConstructor} is of the pattern
// <b>"spring:{bean_name}"</b>
// <P>
// This is the easiest method and produces the best result.  A Collection of Java Beans, such
// as EJB or Hibernate-managed beans, can be directly returned to SmartClient as the result of
// a DMI method, without the need to create an intervening
// +externalLink{http://en.wikipedia.org/wiki/Data_transfer_object,Data Transfer Object} to express
// which fields should be delivered to the browser - instead, only the fields declared on the
// DataSource are returned to the browser (see
// +link{DataSource.dropExtraFields,dropExtraFields}.  In this integration scenario, the
// majority of the features of the SmartClient Server framework still apply - see this
// +link{group:featuresCustomPersistence,overview}.
// <p>
// Note, there are special scoping considerations to bear in mind when using Spring-injected
// DataSources or DMIs - see +link{group:serverDataSourceImplementation,this discussion} of
// caching and thread-safety issues.
// <P>
// <li> <b>configure Spring to return XML or JSON responses</b>: create variants
// on existing Spring workflows that use a different type of View in order to output XML or
// JSON data instead of complete HTML pages.  The SmartClient +link{RestDataSource} provides a
// standard "REST" XML or JSON-based protocol you can implement, or you can adapt generic
// +link{DataSource,DataSources} to existing formats.
// <P>
// In some Spring applications, all existing Spring workflows can be made callable by
// SmartClient with a generic View class capable of serializing the Model to XML or JSON,
// combined with a Controller that always uses this View.  Consider the following Java
// anonymous class, which uses the SmartClient JSTranslater class to dump the entire
// Spring Model as a JSON response.
// <pre>
//  new View() {
//        public void render(Map model, HttpServletRequest request,
//                           HttpServletResponse response) throws IOException {
//                final ServletOutputStream outputStream = response.getOutputStream();
//                response.setContentType("application/x-javascript");
//                outputStream.println(JSTranslater.get().toJS(model));
//                outputStream.close();
//        }
//        public String getContentType() {
//                return "application/x-javascript";
//        }
//  }
// </pre>
// <P>
// If you use this approach, you do not need to install the SmartClient server, and can
// +link{iscInstall,deploy} SmartClient as simple web content (JS/media/HTML files).  If you
// are already familiar with how to generate XML from objects that typically appear in your
// Spring Models, this may be the easiest path.
// </ul>
// <P>
// <b>Using Spring Controllers with SmartClient DMI</b>
// <P>
// You can create a Controller that invokes standard SmartClient server request processing,
// including DMI, like so:
// <pre>
// public class SmartClientRPCController extends AbstractController
// {
//     public ModelAndView handleRequest(HttpServletRequest request,
//                                       HttpServletResponse response)
//         throws Exception
//     {
//         // invoke SmartClient server standard request processing
//         com.isomorphic.rpc.RPCManager.processRequest(request, response);
//         return null; // avoid default rendering
//     }
// }
// </pre>
// This lets you use Spring's DispatchServlet, Handler chain and Controller architecture as a
// pre- and post-processing model wrapped around SmartClient DMI.
//
// @treeLocation Concepts
// @title Integration with Spring
// @visibility external
//<



//> @groupDef hibernateIntegration
//
// SmartClient can integrate with Hibernate in two main ways, both of which are enabled by
// creating a DataSource descriptor (.ds.xml file) with
// +link{DataSource.serverType,serverType="hibernate"}:
// <ul>
// <li> Pre-existing beans: a SmartClient DataSource can be automatically derived from either a
// Hibernate-managed bean or the Hibernate mapping.  Use
// +link{dataSource.schemaBean,schemaBean} to derive from the bean or
// +link{DataSource.autoDeriveSchema} to derive from the mapping.  In this case you will
// initially have a very short .ds.xml per bean - no &lt;fields&gt; are required unless
// and until you want to override the automatically derived fields.
// <li> "Beanless" mode: SmartClient can drive Hibernate as a storage layer only, automatically
// generating Hibernate configuration from a SmartClient DataSource file
// (<i>dataSourceID</i>.ds.xml).  In this case, you do not write a Java bean or create
// Hibernate mappings; Hibernate's beanless
// +externalLink{http://docs.jboss.org/hibernate/orm/3.3/reference/en-US/html/persistent-classes.html#persistent-classes-dynamicmodels,"dynamic model"}
// mode is used.
// </ul>
// <P>
// Which mode to use is primarily a matter of preference and pre-existing code.  However, if
// you do not have pre-existing code or other special circumstances, the following approach is
// the most productive:
// <ol>
// <li> use "beanless" mode, specifying fields in SmartClient's .ds.xml format (far more
// compact than a Java bean with a getter and setter for each field)
// <li> add business logic as needed via DMI, Server Scripting, custom server validators, and
// other approaches covered in the QuickStart Guide
// <li> call any reusable DMI methods both via your SmartClient UI and via other, non-UI
// related Java logic (the DMI methods are now a reusable "data services tier")
// <li> only create an actual Java bean if you discover re-usable, bean-specific business
// logic that cannot be encapsulated as a data service (rare)
// </ol>
// <P>
// Note that the +link{group:adminConsole,Admin Console}'s
// "Import DataSources" section can be used to import test data into serverType:"hibernate"
// DataSources in the same manner as SQLDataSources.
// <p>
// HibernateDataSource supports operations with composite primary keys. Setting data source level property
// +link{attr:dataSource.idClassName,idClassName} to fully qualified class name indicates,
// that entity uses composite primary key.
// <p>
// <h3>Hibernate relations</h3>
// <p>
// For Hibernate integration where Java beans have been explicitly declared,
// HibernateDataSource supports automatic handling of Hibernate relations that don't declare a
// concrete field to hold ID values - see +link{group:jpaHibernateRelations}.
// <p>
// <h3>Hibernate Configuration</h3>
// <p>
// You can provide Hibernate configuration to the SmartClient server in three ways:
// <ul>
// <li>You can place a traditional <code>hibernate.cfg.xml</code> file somewhere on the
//     classpath</li>
// <li>You can have SmartClient look up a Hibernate <code>Configuration</code> to use.  This
//     works in the same way as a +link{ServerObject}, and in fact makes use of the
//     ServerObject code, though note that lookupStyle "attribute" is not supported.  To look
//     up a configuration, add ServerObject-compliant properties to your
//     +link{group:server_properties,server.properties} file, prefixed with <code>hibernate.config</code>.  For
//     example: <pre>
//        hibernate.config.lookupStyle: spring
//        hibernate.config.bean: mySessionFactory
// </pre></li>
// <li>You can provide a Hibernate configuration at the level of individual DataSources, by
//     specifying a +link{DataSource.configBean,configBean} on the dataSource (this is only
//     applicable if you are using Spring; see below)</li>
// </ul>
// If you choose to have SmartClient lookup the Hibernate configuration, and you specify a
// +link{serverObject.lookupStyle,lookupStyle} of "spring", SmartClient will make use of a
// Hibernate <code>SessionFactory</code> configured by Spring.  It is possible to set up multiple
// Hibernate configurations in Spring, and to map individual DataSources to different
// configurations by making use of the <code>dataSource.configBean</code> property mentioned
// above.  Please note the following caveats:
// <ul>
// <li>DataSource-level Hibernate configuration is intended for unusual cases, such as when the
//     physical data store for one DataSource is actually a different database.  Hibernate
//     relations between entities with different configurations do not work</li>
// <li>If you choose to configure Hibernate via Spring, "beanless" on-the-fly
//     mappings are not supported; all entities must be hand-mapped to a bean, either in the
//     properties of the Spring bean providing the configuration, in a <code>.cfg.xml</code>
//     file named in the Spring bean's <code>configLocation</code> property, or by use of
//     persistence annotations in the actual mapped beans themselves</li>
// </ul>
// <p>
// <h3>Manual Hibernate Integration</h3>
// <p>
// In some cases you may not be able to immediately use the built-in HibernateDataSource - in
// this case take a look at +link{group:manualJpaHibernate,manual Hibernate integration}.
//
// @see attr:dataSource.beanClassName
// @see group:sqlConnectionPooling
// @treeLocation Concepts/Persistence Technologies
// @title Integration with Hibernate
// @visibility external
//<

//> @groupDef jpaIntegration
// To use JPA, set serverType="jpa" in your .ds.xml file, then set
// +link{attr:dataSource.beanClassName,beanClassName} to the fully qualified class name of the
// JPA entity.  For example:
// <pre>
// &lt;DataSource
//     ID="countryDS"
//     serverType="jpa"
//     beanClassName="com.smartgwt.sample.showcase.server.jpa.Country"
// &gt;
//     &lt;fields&gt;
// &lt;!-- ... Fields definition ... --&gt;
//     &lt;/fields&gt;
// &lt;/DataSource&gt;
// </pre>
// +link{DataSource.autoDeriveSchema} is supported for deriving DataSource fields from JPA
// entities automatically (except with JPA 1.0; see below).
// <p>
// Full support is provided for executing simple +link{Criteria}, with +link{AdvancedCriteria}
// supported if you have Power Edition or above.  However, note that there are limitations with case
// sensitive search in MySQL since MySQL automatically uses the 'like' operator in a
// case-insensitive manner and JPA does not correct this.  See <a
// href="http://dev.mysql.com/doc/refman/5.5/en/case-sensitivity.html">MySQL Reference Manual
// :: C.5.5.1 Case Sensitivity in String Searches</a> for more information.<p/>
// <p>
// If create a custom DataSource based on the built-in JPA functionality, subclass
// <code>com.isomorphic.jpa.JPA2DataSource</code>.
// <p>
// <h3>JPA configuration</h3>
// <p>
// JPA configuration should be specified in the <code>persistence.xml</code> file as usual, and placed
// in the <code>/WEB-INF/classes/META-INF</code> directory.  For JPA 2.0 make sure you correctly
// declare its usage in <code>persistence.xml</code>:<pre>
// &lt;persistence
//     version="2.0"
//     xmlns="http://java.sun.com/xml/ns/persistence"
//     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
//     xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
//                         http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
// &gt;
// ...
// &lt;/persistence&gt;</pre><br/>
// <P>
// JPADataSource supports operations with composite primary keys. Setting data source level property
// +link{attr:dataSource.idClassName,idClassName} to fully qualified class name indicates,
// that entity uses composite primary key.
// <P>
// JPADataSource supports automatic handling of JPA relations that don't declare a concrete
// field to hold ID values - see +link{group:jpaHibernateRelations}.
// <P>
// <h3>JPA 1.0 compatibility</h3>
// <P>
// To use JPA 1.0, set serverType="jpa1" instead.  JPA 1.0 does not support
// +link{dataSource.autoDeriveSchema}.  For JPA 1.0, the concrete implementation class (for subclassing to
// create a custom DataSource) is <code>com.isomorphic.jpa.JPADataSource</code>.
// <p>
// <h3>JPA transactions</h3>
// <p>
// JPA provides three mechanisms for transactions: for JEE applications JPA provides integration
// with JTA (Bean Managed Transactions and Container Managed Transactions); for JSE applications JPA has a native
// <code>EntityTransaction</code> implementation (Locally Managed Transactions). Spring framework is another popular
// way for declaring transactions in application.
// The transaction mechanism should be configured in the +link{group:server_properties,server.properties}
// file by setting
// property <b><code>jpa.emfProvider</code></b> to the fully qualified class name of the provider
// (implementation of <code>com.isomorphic.jpa.EMFProviderInterface</code>). SmartClient comes
// with five implementations:<ul>
// <li><b><code>com.isomorphic.jpa.EMFProviderLMT</code></b> - for Locally Managed Transactions.
//      Every fetch or DML operation starts a new transaction and commits after successful
//      execution.<br/>
//      This implementation reads the <b><code>jpa.persistenceUnitName</code></b> property from
//      the +link{group:server_properties,server.properties} file.  The value of this property needs
//      to be set to
//      the name of the persistence unit configured in <code>persistence.xml</code> file. For example:<pre>
// jpa.persistenceUnitName: PERSISTENCE_UNIT_NAME
//      </pre></li>
// <li><b><code>com.isomorphic.jpa.EMFProviderBMT</code></b> - for Bean Managed Transactions.
//      Every fetch or DML operation acquires the transaction object from the container and starts it.<br/>
//      This implementation reads two properties from the +link{group:server_properties,server.properties}
//      file:
//      <b><code>jpa.entityManager</code></b> and <b><code>jpa.entityManagerFactory</code></b>
//      containing appropriate resource name references configured in
//      <code>/WEB-INF/web.xml</code>. Configuration example:<pre>
// &lt;!-- EntityManager resource reference name declaration --&gt;
// &lt;persistence-context-ref&gt;
//    &lt;persistence-context-ref-name&gt;persistence/em&lt;/persistence-context-ref-name&gt;
//    &lt;persistence-unit-name&gt;PERSISTENCE_UNIT_NAME&lt;/persistence-unit-name&gt;
// &lt;/persistence-context-ref&gt;
//
// &lt;!-- EntityManagerFactory resource reference name declaration --&gt;
//  &lt;persistence-unit-ref&gt;
//      &lt;persistence-unit-ref-name&gt;persistence/emf&lt;/persistence-unit-ref-name&gt;
//      &lt;persistence-unit-name&gt;PERSISTENCE_UNIT_NAME&lt;/persistence-unit-name&gt;
//  &lt;/persistence-unit-ref&gt;
//
// #Property values for sample references:
// jpa.entityManager: persistence/em
// jpa.entityManagerFactory: persistence/emf
//      </pre></li>
// <li><b><code>com.isomorphic.jpa.EMFProviderCMT</code></b> - for Container Managed Transactions.
//      Every fetch or DML operation acquires the transaction object from the JEE container.
//      After successful method execution the container commits the transaction. In case of execution
//      failure <code>tx.setRollbackOnly()</code> is used to notify container to rollback the
//      transaction.<br/>
//      This implementation reads two properties from the +link{group:server_properties,server.properties}
//      file:
//      <b><code>jpa.entityManager</code></b> and <b><code>jpa.entityManagerFactory</code></b>
//      containing appropriate resource name references configured in
//      <code>/META-INF/ejb-jar.xml</code>. Configuration example:<pre>
// &lt;?xml version="1.0" encoding="UTF-8"?&gt;
// &lt;ejb-jar
//      version = "3.0"
//      xmlns = "http://java.sun.com/xml/ns/javaee"
//      xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
//      xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"&gt;
//      &lt;enterprise-beans&gt;
//          &lt;session&gt;
//              &lt;ejb-name&gt;TestEJB&lt;/ejb-name&gt;
//              &lt;persistence-context-ref&gt;
//                  &lt;persistence-context-ref-name&gt;persistence/em&lt;/persistence-context-ref-name&gt;
//                  &lt;persistence-unit-name&gt;PERSISTENCE_UNIT_NAME&lt;/persistence-unit-name&gt;
//              &lt;/persistence-context-ref&gt;
//              &lt;persistence-unit-ref&gt;
//                  &lt;persistence-unit-ref-name&gt;persistence/emf&lt;/persistence-unit-ref-name&gt;
//                  &lt;persistence-unit-name&gt;PERSISTENCE_UNIT_NAME&lt;/persistence-unit-name&gt;
//              &lt;/persistence-unit-ref&gt;
//         &lt;/session&gt;
//     &lt;/enterprise-beans&gt;
// &lt;/ejb-jar&gt;
//
// #Property values for sample references:
// jpa.entityManager: persistence/em
// jpa.entityManagerFactory: persistence/emf
//      </pre></li>
// <li><b><code>com.isomorphic.jpa.EMFProviderNoTransactions</code></b> - transactions are
//      not used.<br/>
//      From the +link{group:server_properties,server.properties} file this implementation reads the
//      <b><code>jpa.persistenceUnitName</code></b> property which must containt the name of persistence unit
//      configured in <code>persistence.xml</code> file. For example:<pre>
// jpa.persistenceUnitName: PERSISTENCE_UNIT_NAME
//      </pre></li>
// <li><b><code>com.isomorphic.jpa.EMFProviderSpring</code></b> - for Spring Framework managed Transactions.
//      Every fetch or DML operation acquires the transaction object from the Spring Application Context.<br/>
//      This implementation reads two properties from the +link{group:server_properties,server.properties}
//      file:
//      <b><code>jpa.entityManagerFactory</code></b> and <b><code>jpa.transaction</code></b>
//      containing appropriate bean names configured in Spring Application Context.
//      You have to declare additional bean in your Spring Application Context to allow SmartClient
//      to acquire reference to context.
//      Configuration example:<pre>
// &lt;!-- SpringApplicationContextProvider bean definition required to get access to application context. --&gt;
// &lt;bean id="springApplicationContextProvider" class="com.isomorphic.spring.SpringApplicationContextProvider" /&gt;
//
// &lt;!-- Connection to data base --&gt;
// &lt;bean id="dataSource"
//      class="org.springframework.jdbc.datasource.DriverManagerDataSource"
//      p:driverClassName="DRIVER_CLASS"
//      p:url="CONNECTION_URL"
//      p:username="DB_USER_NAME"
//      p:password="DB_USER_PASSWORD" /&gt;
//
// &lt;!-- Reference to JPA EntityManagerFactory --&gt;
// &lt;bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"&gt;
//     &lt;property name="dataSource" ref="dataSource" /&gt;
//     &lt;property name="jpaVendorAdapter"&gt;
//        &lt;bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"&gt;
//            &lt;property name="database" value="DB_TYPE" /&gt;
//        &lt;/bean&gt;
//     &lt;/property&gt;
//     &lt;property name="persistenceUnitName" value="PERSISTENCE_UNIT_NAME" /&gt;
// &lt;/bean&gt;
//
// &lt;!-- Reference to JpaTransactionManager --&gt;
// &lt;bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"&gt;
//     &lt;property name="entityManagerFactory" ref="entityManagerFactory" /&gt;
// &lt;/bean&gt;
//
// #Property values for sample bean names:
// jpa.entityManagerFactory: entityManagerFactory
// jpa.transaction: transactionManager
//      </pre></li>
// </ul>
// You can set <b><code>jpa.emfProvider</code></b> to your own implementation of
// <code>com.isomorphic.jpa.EMFProviderInterface</code> if you have specific requirements for
// transaction handling. <code>EMF</code> will instantiate provided implementation on initialization (static) and
// will use same instance every time. By using own implementation you can have complete control over creating/using
// <code>EntityManagerFactory</code> and <code>EntityManager</code> instances.<p/>
// <b>Additional configurations:</b><p/>
// In case you have several persistence units defined in your <code>persistence.xml</code> you can have additional
// configurations in +link{group:server_properties,server.properties} file. Additional configurations
// (prefixed with <b><code>jpa.</code></b>) should have name, <b><code>emfProvider</code></b> property and other
// properties required by specified EMF provider implementation.
// For example:<pre>
// jpa.configName.emfProvider: com.isomorphic.jpa.EMFProviderLMT
// jpa.configName.persistenceUnitName: ANOTHER_PERSISTENCE_UNIT_NAME</pre>
// To use additional JPA configuration you have to set <b><code>jpaConfig</code></b> property in data source
// definition:<pre>
// &lt;DataSource
//     ID="countryDS"
//     serverType="jpa"
//     beanClassName="com.smartgwt.sample.showcase.server.jpa.Country"
//     jpaConfig="configName"
// &gt;</pre>
// <b>Transaction management:</b>
// <p/><ul>
// <li>Operating under +link{RPCManager} (<code>+link{DSRequest}</code> has reference to
//   <code>+link{RPCManager}</code>):<ul>
//      <li>If participating in automatic transactions:<ul>
//          <li>retrieves existing transaction from <code>+link{RPCManager}</code> (if available);</li>
//          <li>starts new transaction (if not found in <code>+link{RPCManager}</code>);</li></ul></li>
//      <li>If one transaction per operation is used - starts new transaction;</li>
//      <li>Registers itself to <code>DSRequest.registerCallback()</code> for <code>onSuccess()</code>/
//      <code>onFailure()</code> execution to commit/roll back transaction;</li>
//      <li>Sets <code>DSRequest.setFreeOnExecute()</code> to <code>false</code> to postpone releasing
//          of <code>EntityManager</code> avoiding lazy loading exceptions when creating JS response and
//          traversing through persistent object tree;</li>
//      <li>Registers itself to <code>RPCManager.registerFreeResourcesHandler()</code> for <code>freeResources()</code>
//      execution to release <code>EntityManager</code>.</li></ul><br/>
// If you want to use same <code>EntityManager</code> and transaction in your custom data source implementation you can
// acquire it by <pre>JPAConnectionHolder holder = DataSource.getTransactionObject(req, EMF.TRANSACTION_ATTR);</pre>
// <code>JPAConnectionHolder</code> instance contains references to entity manager and transaction object
// used by <code>JPADataSource</code>. You should never commit/rollback automatic transaction.
// Overall commit/rollback will be issued by <code>RPCManager</code> and will be handled by <code>JPADataSource</code>
// object which started transaction.
// </li>
// <li>Operating without +link{RPCManager}:<ul>
//      <li>starts new transaction;</li>
//      <li>commits/rolls back transaction and releases <code>EntityManager</code> if
//          <code>DSRequest.setFreeOnExecute()</code> is set to <code>true</code> (defalut);</li>
//      <li>relies on calling code to call <code>onSuccess()</code>/<code>onFailure()</code> to commit/roll back
//          transaction and to call <code>freeResources()</code> to release <code>EntityManager</code>.<br/>
//          Example code for data source operation execution with manual transaction handling:
//          <pre>
//              DSRequest req = new DSRequest("myDS", "fetch");
//              req.setFreeOnExecute(false);
//              DSResponse resp = req.execute();
//              List dataList = resp.getDataList();
//              //... traverse through persistent object tree
//              // Commit current transaction.
//              ((JPADataSource) r.getDataSource()).onSuccess();
//              // Release entity manager.
//              ((JPADataSource) r.getDataSource()).freeResources(req);
//          </pre></li></ul>
// </li></ul>
// <p>
// <h3>Manual JPA Integration</h3>
// <p>
// In some cases you may not be able to immediately use the built-in JPADataSource - in
// this case take a look at +link{group:manualJpaHibernate,manual Hibernate integration}.
//
// @see group:sqlConnectionPooling
// @treeLocation Concepts/Persistence Technologies
// @title Integration with JPA
// @visibility external
//<

//> @groupDef jpaHibernateRelations
// JPA and Hibernate allow relations to be declared between entities where there is no actual
// Java field for storing the ID of a related entity, even though such a column exists in the
// database.  For example:
// <pre>
//    &#64;ManyToOne
//    &#64;JoinColumn(name="countryId", referencedColumnName="countryId")
//    private Country country;
// </pre>
// JPADataSource and HibernateDataSource support this style of declaration and will
// automatically handle mapping between IDs and entities.
// <p>
// The example above and the following examples assume a DataSource "country" for a
// JPA/Hibernate entity "Country" with an Id field of "countryId", and a DataSource "city" for a
// JPA/Hibernate entity "City" with an Id field of "cityId".
// <p>
// <h3>Many-To-One Relations</h3>
// <p>
// An example of Many-To-One is that Many "City"s belong to One "Country".  In Java, each City
// bean has a field of type Country.  In the database, rows for cities and countries are linked
// by ID.
// <p>
// To specify a many-to-one relation, declare a DataSourceField named after the Java field that
// declares the relation ("country" above) with the property
// +link{attr:DataSourceField.foreignKey,foreignKey} pointing to related DataSource's primary key:
// <pre>
//    &lt;field name="country" foreignKey="country.countryId"/&gt;
// </pre>
// When delivered to the browser, the value of the <code>country</code> field will be the ID of
// the related Country entity.  The <code>country</code> field can be treated as a normal text
// or integer field value, for example, you can use a +link{SelectItem} that uses
// +link{SelectItem.optionDataSource} to allow selecting the ID of a different related
// entity.  Then, when the new ID is saved to the server, JPADataSource automatically looks up
// the related object and persists the new relation to JPA.
// <p>
// <b>NOTE:</b>: do not declare a "type" attribute for such a field - these fields provide
// specialized mapping between IDs and JPA/Hibernate entities, so don't really have a single
// concrete type.
// <p>
// If you want fields from a related entity to be included whenever your entity is fetched, for
// example, whenever a city is fetched you want the <code>countryName</code> fetched from the
// related <code>country</code> entity, use +link{DataSourceField.includeFrom}.
// <p>
// <b>Automatic Criteria translation</b>
// <p>
// If criteria are submitted for a ManyToOne relation field containing an ID value, this will
// correctly return Records that are associated with the related object that has that ID.
// <p>
// For example, given a countryId of "1", you can fetch all city Records that are related to
// that countryId as follows:
// <smartclient>
// <pre>
//   isc.DataSource.get("city").fetchData({ countryId: 1 }, <callback>);
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
//    DataSource.get("city").fetchData(new Criteria("countryId", "1"), <callback>);
// </pre>
// </smartgwt>
// <p>
// <h3>One-to-Many Relations</h3>
// <p>
// An example of One-To-Many relation is that One "Country" has Many "City"'s.  Each "Country"
// has a list of cities within it, which may be declared as a Java bean property of Collection
// type (or List, Set, etc).
// <p>
// To specify a one-to-many relation, declare a DataSourceField that:
// <ul>
// <li> is named after the Java field that declares the OneToMany relation (whose type is a
//      Collection of the related entities)
// <li> declares its "type" to be the ID of the related DataSource
// <li> declares a +link{attr:DataSourceField.foreignKey,foreignKey} pointing to the related
//      DataSource's primaryKey field
// <li> sets multiple="true"
// </ul>
// For example, for a Country bean that has a Collection of City beans:
// <pre>
//     &lt;field name="cities" type="city" multiple="true" foreignKey="city.cityId"/&gt;
// </pre>
// With this declaration, whenever Records are loaded from the Country DataSource, they will
// contain a list of City Records as subobjects, accessible via
// <smartgwt><code>countryRecord.getAttributeAsRecordList("cities")</code></smartgwt>
// <span class="client">countryRecord.cities</span>
// <p>
// If loading all related "city" records is desirable in some circumstances and not others, you
// can use +link{operationBinding.outputs} to avoid loading "city" records for certain
// operations.
// <p>
// <h3>Many-To-Many Relations</h3>
// An example of Many-To-Many relation is that Students have multiple Courses and each Course has
// multiple Students. In Java each Student bean has a list of Courses and each Course bean has a
// list of Students. In database tables are linked using additional table holding references to
// both students and courses.
// <P>
// To set up Many-To-Many relation between data sources you need to set up One-To-Many relation on
// both sides.
// <P>
// For example students DataSourceField for CourseDS data source:
// <pre>
//     &lt;field name="students" type="integer" foreignKey="StudentDS.id" multiple="true" /&gt;
// </pre>
// and courses DataSourceField for StudentDS data source:
// <pre>
//     &lt;field name="courses" type="integer" foreignKey="CourseDS.id" multiple="true" /&gt;
// </pre>
// Note that type attribute can be safely omitted here.
// <P>
// <b>Note</b> that alternative type declaration to be ID of related data source (as in regular
// One-To-Many relation case) would work as expected, but is <b>not recommended</b> to use, cause
// it would result in getting lots of copies of same data. Smartclient server will prevent infinite
// loops, but still lots of unnecessary data will be sent to client.
// <P>
// <h3>Alternative: Many-To-One loading complete related object</h3>
// <p>
// For a Many-To-One relation, instead of loading just the ID of the related object, you can
// load the entire related object as a nested Record.  To do so, just declare the type of the
// field to be the ID of the related DataSource, rather than leaving type unset:
// <pre>
//     &lt;field name="country" type="country" foreignKey="country.countryId"/&gt;
// </pre>
// The nested "country" Record will be available on a "city" record via
// <smartgwt><code>cityRecord.getAttributeAsRecord("country")</code></smartgwt>
// <smartclient>cityRecord.country</smartclient>.
// Saving a City record that contains a nested Country record in the "country"
// attribute will result in the Country being updated in JPA/Hibernate.
// <P>
// This mode is not typically used, since loading just the ID of the related Country object is
// more efficient if many cities are being loaded, and the related Country object can always be
// loaded with a second fetchData() data, which can still be done in a single HTTP request via
// +link{isc.RPCManager.startQueue(),queuing}.
// <P>
// <h3>Alternative: One-To-Many loading related IDs</h3>
// <p>
// For a One-To-Many relation, instead of loading the complete list of related objects, you can
// load just a list of their IDs.  To do so, just omit the type declaration when declaring the
// relation:
// <pre>
//    &lt;field name="cities" multiple="true" foreignKey="city.cityId"/&gt;
// </pre>
// When saving, if a replacement list of IDs is included in the Record, the appropriate
// JPA/Hibernate relationships will be updated.
// <P>
// This is very rarely used, and would typically only be used by client-side code that plans to
// programmatically work with the list of related IDs rather than display them in a UI
// component.
// <p>
// <h3><b>NOTE:</b> Bidirectional relations</h3>
// <p>
// When relations are declared, JPA and Hibernate consider only one of the two entities to be
// the "owner" of the relation, meaning essentially that the references that make up the
// relationship are stored with that entity.  When performing updates, make sure you update the
// entity that "owns" the relation.  All changes to relations on "non-owning" entities are
// silently discarded.
// <p>
// <h3>Search criteria on One-to-Many and Many-to-Many relations</h3>
// <p>
// The following +link{Operator,search operators} are supported with the behaviors listed
// below.  For simple Criteria, criteria values are treated identically to the "equals"
// operator and the +link{dsRequest.textMatchStyle,textMatchStyle} is ignored.
// <P>
// Examples are given in terms of a "country" DataSource that has a one-to-many relation with a "city"
// DataSource through a relation field called "cities"
// <p>
// <table border=1 class="normal">
// <tr>
// <td>Operator</td><td>Behavior</td>
// </tr>
// <tr>
// <td><code>isNull</code></td><td>matches country records which have no related cities</td>
// </tr>
// <tr>
// <td><code>notNull</code></td><td>matches country records which have at least one related city</td>
// </tr>
// <tr>
// <td><code>equals, notEqual</code></td><td>
// <code>criterion.value</code> should be a primaryKey value for the related
// city DataSource.  This criterion matches any country which
// contains the passed city (or for "notEqual", matches any country which
// <b>does not</b> contain the passed city.
// </td>
// </tr>
// <tr>
// <td><code>inSet, notInSet</code></td><td>
// <code>criterion.value</code> should an array of primaryKey values for the
// related city DataSource.  This criterion matches any country which
// contains <b>any</b> of the passed cities (or for "notInSet", which
// contains <b>none</b> of the passed cities).</td>
// </tr>
// </table>
// The following is an example of a criterion for matching a "country" records which have related
// cities with primaryKey values 1, 2 or 3 (shown serialized as JSON):
// <pre>
//   {fieldName:"cities", operator:"inSet", value:[1,2,3]}
// </pre>
// As an alternative syntax, "equals", "notEqual", "inSet" and "notInSet" will also allow
// <code>criterion.value</code> to be specified as a list of Objects, each containing the
// primaryKey field and its value:
// <pre>
//      {fieldName:"cities",operator:"inSet",value:[{cityId:1},{cityId:2},cityId:3]}
// </pre>
// The operators explained above work the same way for Many-To-Many relation fields.
// <p>
// Any other operator applied to a relation field will cause a warning to be logged, and will be
// treated as though the criterion were not present (matches all records).
//
// @treeLocation Concepts/Persistence Technologies
// @title JPA &amp; Hibernate Relations
// @visibility external
//<

//> @groupDef manualJpaHibernate
// In some cases you may not be able to immediately use the built-in JPADataSource or
// HibernateDataSource.  For example, you have pre-existing business logic that already
// directly calls JPA or Hibernate APIs, and it may seem to be a large task to refactor this
// logic so that the built-in DataSources can be used.
// <p>
// In this case you can use the overall approach described in
// +link{group:serverDataIntegration} to connect to your pre-existing business logic.  You will
// not be leveraging any of the built-in JPA or Hibernate functionality, so the approach and level of
// effort will be the same as if you were using a non-Hibernate ORM or integrating with
// entirely custom Java classes.
// <P>
// +explorerExample{reusableORMDataSource,This example} shows a Hibernate-based implementation of
// a custom DataSource that implements support for simple Criteria, sorting and data paging,
// but not +link{AdvancedCriteria}, +link{dataSourceField.includeFrom,automatic joins},
// automatic transactions or many other features built into HibernateDataSource.
// <P>
// Because these features are very valuable and more features are added to the built-in
// DataSources all the time, it's recommended that you refactor your code to use the built-in
// DataSources as soon as you can; refactoring existing business logic as validators, DMIs, or
// custom DataSources is often easier than it looks.
// <P>
// You can also take the approach of using the built-in DataSources for new entities or
// entities where there is currently no significant business logic, while continuing to use
// your existing code where it's non-trivial to refactor.
//
// @title Manual JPA &amp; Hibernate Integration
// @treeLocation Concepts/Persistence Technologies
// @visibility external
//<


//> @groupDef gaeIntegration
// Google App Engine (GAE) is a platform-as-a-service (PaaS) offering from Google, which
// supports cloud deployment of applications written in Java and other languages, as well as
// SQL or JPA access to highly scalable storage (both Google CloudSQL and BigTable-based
// solutions).
// <p>
// SmartClient applications, including those based on the SmartClient Server Framework, can be
// deployed to GAE and can integrate with Google CloudSQL either via SQLDataSource or via JPA,
// and can also use Google's BigTable-based storage via JPA.
// <P>
// For further information, setup instructions and example configuration, see our
// +externalLink{http://wiki.smartclient.com/display/Main/Google+App+Engine,public wiki}
//
// @see group:jpaIntegration
// @treeLocation Concepts/Persistence Technologies
// @title Google Application Engine (GAE)
// @visibility external
//<

//> @groupDef sqlConnectionPooling
// Many databases will automatically close inactive connections, which can interfere with
// connection pooling: if an application is not constantly using all of the connections in its
// pool, it may retrieve a closed connection from the pool.
// <P>
// In some cases you can disable the behavior of closing inactive connections.  For MySQL it's
// controlled by the
// +externalLink{http://dev.mysql.com/doc/refman/5.0/en/server-system-variables.html#sysvar_wait_timeout,wait_timeout}
// setting in your my.cnf file).  However, this could potentially cause leaked connections if
// applications terminate without cleaning up their database connections.
// <P>
// Intelligent connection pools compensate for unexpectedly closed connections automatically:
// <ul>
// <li>J2EE containers generally implement internal keepalives or staleness checks - this is
// the preferred solution if available.  If using SQLDataSource, use JNDI-based
// configuration as described +link{group:adminConsole,here}.
// <li>SQLDataSource uses DBCP (Apache Commons) pooling, which also compensates for connection
// closure automatically.  This is enabled by default with appropriate settings, but can be
// disabled system wide via setting <b>sql.pool.enabled</b> to false in
// +link{group:server_properties,server.properties}, or
// disabled for a specific database configuration via <b>sql.<i>dbName</i>.pool.enabled</b>.
// The following properties can also be set on sql.pool / sql.<i>dbName</i>.pool and control
// same-named DBCP properties, however, it is not recommended to set these properties unless
// you have experience with DBCP and are troubleshooting a specific pool-related performance
// problem: testOnBorrow, testOnReturn, testWhileIdle, timeBetweenEvictionRunsMillis,
// minEvictableIdleTimeMillis, numTestsPerEvictionRun.
// <p>
// If you are trying to diagnose an issue related to SQL connection pooling, you can enable
// DEBUG logging for the following classes in <code>log4j.isc.config.xml</code> (see
// installation instructions for details about this file).  All of these classes are in package
// <code>com.isomorphic.sql</code>:
// <ul>
// <li> PoolableSQLConnectionFactory: logs connection creation, and whether or not the
//      connections are pooled
// <li> SQLConnectionManager: logs when connections are borrowed
// <li> SQLDriver: logs the hashCode of the connection when SQL statements are executed
// <li> SQLTransaction: logs transactional open, commit, rollback and close.
// </ul>
// <li>JPA/Hibernate: Hibernate's built-in connection pool is <b>not</b> intended for
// production use according to Hibernate's own documentation.  This includes using JPA with
// Hibernate as the provider. If you get dead connections during development you can disable
// Hibernate's built-in connection pool by setting "hibernate.connection.pool_size" to 0. For
// production use you must use production-ready connection pool libraries for example
// C3P0. Here are recommended settings for C3P0 properties:
// <ul>
// <li>c3p0.acquireRetryDelay=1000</li>
// <li>c3p0.acquireRetryAttempts=60</li>
// <li>c3p0.breakAfterAcquireFailure=false</li>
// </ul>
// </li>
// </ul>
// @treeLocation Concepts/Persistence Technologies
// @title SQL Connection Pooling
// @visibility external
//<

//> @groupDef standaloneDataSourceUsage
// The DataSource layer can be used in a standalone application separate from a servlet engine,
// for things like Java Swing applications, batch data processing or sending emails based on
// database state.
// <p>
// Note that we do still require <code>servlet-api.jar</code> to be on the classpath of
// standalone applications.  This is only to satisfy class-loading requirements; there are
// cases where running inside a servlet engine allows us to provide extra functionality, such
// as declarative authentication and authorization features, so some classes do have member
// variables or method parameters of types in the Servlet API.  They all operate correctly
// when these variables or parameters are null, but the JVM will still try to load the classes
// and will crash if they are missing.
// <p>
// Using the DataSource layer to run DataSource operations in your standalone applications is
// extremely straightforward.  This example fetches and prints every record from the
// "customers" DataSource:
// <pre>
//   public static void main(String[] args) {
//     DataSource ds = DataSourceManager.get("customers");
//     List records = ds.fetch();
//     for (Iterator i = records.iterator; i.hasNext(); ) {
//       System.out.println(i.next());
//     }
//   }
// </pre>
// To make this example fetch just customers in the United States:
// <pre>
//     Map criteria = new HashMap();
//     criteria.put("countryCode", "US");
//     List records = ds.fetch(criteria);
// </pre>
// This example shows how to run a specific fetch operation, specifying both selection
// criteria and a sort order, using a <code>DSRequest</code> rather than a <code>DataSource</code>
// convenience method:
// <pre>
//   public static void main(String[] args) {
//     DSRequest dsReq = new DSRequest("customers", "fetch");
//     dsReq.setOperationId("specialFetch");
//     Map criteria = new HashMap();
//     criteria.put("countryCode", "US");
//     criteria.put("region", "CA");
//     dsReq.setCriteria(criteria);
//     dsReq.setSortBy("customerName");
//     List records = dsReq.execute().getDataList();
//   }
// </pre>
// This example shows how to do a simple update:
// <pre>
//   public static void main(String[] args) {
//     DataSource ds = DataSourceManager.get("customers");
//     Map criteria = new HashMap();
//     criteria.put("customerNo", 12345);
//     Map values = new HashMap();
//     values.put("creditLimit", 10000);
//     values.put("currencyCode", "USD");
//     ds.update(criteria, values);
//   }
// </pre>
// Finally, this example shows how to perform a specific update operation via a
// <code>DSRequest</code>:
// <pre>
//   public static void main(String[] args) {
//     DSRequest dsReq = new DSRequest("customers", "update");
//     dsReq.setOperationId("specialUpdate");
//     Map criteria = new HashMap();
//     criteria.put("customerNo", 12345);
//     Map values = new HashMap();
//     values.put("creditLimit", 10000);
//     values.put("currencyCode", "USD");
//     dsReq.setCriteria(criteria);
//     dsReq.setValues(values);
//     dsReq.execute();
//   }
// </pre>
// <b>NOTES</b>
// <p>
// Because we are not running inside a servlet container, SmartClient's built-in logic to
// work out where its application root is does not work.  Therefore, you need to manually
// set a "webRoot" in your +link{group:server_properties,server.properties} file.
// The webRoot should point
// to the root folder of your application (note for SmartGWT applications, this is typically
// the "war" subfolder of your project).  Example entries:<p>
// <code>    webRoot: /home/testUser/myProject</code><p>
// or:<p>
// <code>    webRoot: C:\Projects\MyProject\war</code><p>
// Again in +link{group:server_properties,server.properties},
// you may need to set <code>isomorphicPathRootRelative</code>
// to match the standalone project's layout if you make the standalone mode separate:<p>
// <code>    isomorphicPathRootRelative: myProject/sc</code>
// <p>
// <smartgwt>
// Note that the directory structure that normally appears in "war/<i>myProject</i>/sc" in your
// GWT project is required to be present under the configured
// <i>webRoot</i>+<i>isomorphicPathRootRelative</i> directory when running standalone.  This structure is
// normally created by the GWT compiler, by processing &lt;inherits&gt; tags in your .gwt.xml
// that cause resources to be copied from smartgwtee.jar.  If your build/deployment scripts do
// not invoke the GWT compiler these files will not be present, so you will need to either add
// a step to invoke the GWT compiler or manually copy the files.
// </smartgwt>
// You should place the +link{group:server_properties,server.properties} file somewhere on your classpath.
// Typically, the root of your <code>bin</code> or <code>classes</code> folder structure is
// the most suitable place for it.
// <p>
// Both the built-in DataSources and custom DataSources can be used in standalone
// applications, <b>but only if you do not introduce dependencies on the servlet container in
// your DataSource code</b>. For example, if you have a security check in a DMI or custom
// DataSource that depends on checking the current logged-in user, code in a means of bypassing
// this, or create a parallel operationBinding that is accessible only to the superuser.
// <p>
// When you use the DataSource layer in a standalone application,
// +link{dataSource.requiresAuthentication,Declarative Security} is disabled; all operations
// are considered to be valid for use in a standalone application.  Other DataSource features
// that are disabled because they are not relevant in a standalone context are:
// <ul>
// <li>+link{DSRequestModifier.value,Transaction Chaining}</li>
// <li>+link{DataSource.autoJoinTransactions,Transactional persistence}</li>
// </ul>
// <br>
// <b>Using Spring in a standalone application</b>
// <p>
// In a typical web application, Spring configuration is picked up from an "applicationContext"
// file by a servlet or listener, and then made available to the rest of the app via the
// servletContext.  When running standalone, this is not possible, so instead we read the
// applicationContext file manually when we need to, eg, create a DataSource object that is
// configured as a Spring bean.
// <p>
// By default, the framework will look in the "normal" place for for this configuration:
// <code>WEB-INF/applicationContext.xml</code>.  If you have changed the location or name
// of this file, and you want to run the application outside of a servlet engine, you can tell
// the framework where to find the configuration file by specifying property
// <code>standalone.spring.applicationContext</code> in your +link{group:server_properties,server.properties}.
// The default setting for this property looks like this:<pre>
//    standalone.spring.applicationContext: $webRoot/WEB-INF/applicationContext.xml
// </pre>
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title Standalone DataSource Usage
// @visibility external
//<



//> @groupDef sqlDataSource
// The SmartClient Server supports comprehensive, codeless SQL connectivity for DataSources
// defined in XML.  Our SQL connectivity is mature, feature-rich, protected against injection
// attacks and easily customizable to support user-written SQL and additional business logic
// of any complexity.  +link{group:sqlVsJPA,This article} compares the built-in SQL DataSource
// to other persistence approaches based on Javabeans.
// <P>
// To use the built-in SQL engine, declare a +link{DataSource} in XML format with
// +link{dataSource.serverType} set to "sql", and place it in the shared dataSources directory
// ([webroot]/shared/ds by default) in a file called "[dataSourceId].ds.xml".
// <p>
// You can then use the +link{group:adminConsole,Admin Console} to configure database access,
// as well as to automatically create and populate a database table based on your DataSource.
// By default, DataSources will use the "default database" from the admin console, however you
// can set +link{dataSource.dbName} to the name of a specific database configuration you have
// configured via the Admin Console.
// <P>
// The list of databases known to work with the the built-in SQL logic is as follows:
// <p>
// <table class='normal'>
// <tr><td width=40></td><td width=400><i>HSQLDB 1.7.x, 1.8.x, 2.0.x, 2.2.x, 2.3.x</i></td><td></td></tr>
// <tr><td></td><td><i>IBM DB2 8.x, 9.x, 10.x</i></td><td></td></tr>
// <tr><td></td><td><i>IBM DB2 for i (formerly known as DB2 for i5/OS) V5R4M0 onwards</i></td><td></td></tr>
// <tr><td></td><td><i>Firebird 2.5</i></td><td></td></tr>
// <tr><td></td><td><i>Informix 11.5+, 12.1</i></td><td></td></tr>
// <tr><td></td><td><i>MS SQL Server 2000, 2005, 2008, 2008 R2, 2012, 2014</i></td><td></td></tr>
// <tr><td></td><td><i>MySQL 3.2.x, 4.0.x, 4.1.x, 5.0.x, 5.1.x, 5.5.x, 5.6x</i></td><td></td></tr>
// <tr><td></td><td><i>Oracle 8.0.5, 8i, 9i, 10g, 11g, 12c</i></td><td></td></tr>
// <tr><td></td><td><i>PostgreSQL 7.x, 8.x, 9.x</i></td><td></td></tr>
// </table>
// <p>We also support a generic SQL92 database connection which works for basic CRUD operations
// with any database product that supports standard SQL92 syntax and data types, plus a couple
// of widely-implemented features that are not actually part of the standard.  Specifically,
// this means we do not support:<ul>
// <li>Sequences</li>
// <li>Paging via SQL limit queries</li>
// <li>+link{DataSource.autoJoinTransactions,Automatic transaction management}
// <li>Long text values (there is no real definition of "long" here - we try to use a standard
// VARCHAR, but different databases will support different maximum values for this)</li>
// <li>Databases that do not implement the widely-supported LOWER() function</li>
// <li>Databases that do not support the ability to perform string-type operations on numeric
// columns - for example, <code>myNumericColumn LIKE '%5%'</code></li>
// </ul>
// <p>You will also need a JDBC driver for your specific database. Licensing restrictions
// prevent us including any JDBC driver other than the one for HSQLDB.  However, you
// can download these drivers for free from the vendors' websites.  If your specific database
// server or version is not listed above, please go to the
// <a href="http://forums.smartclient.com">SmartClient forums</a> for assistance.
// <P>
// You can also connect to an existing database table - the Visual Builder DataSource Wizard
// ("New" button in DataSources pane) allows you to pick an existing database table and have a
// DataSource generated from it.  The Batch DataSource Generator tool will allow you to
// generate DataSources for any number of SQL DataSources, and customize the process so that
// organization-specific rules are applied (eg ignoring certain columns, or using naming
// conventions to generate more complete DataSources).
// <P>
// Once you have your SQL DataSource connected to a table, in a default SDK installation,
// DSRequests for your DataSource will be sent to the default
// +link{RPCManager.actionURL,actionURL}, and hence handled automatically, without you having
// to write any Java code, by the +link{servletDetails,IDACall servlet registered in web.xml}.
// IDACall is a very simple servlet that just calls the server-side method
// dsRequest.execute() for all inbound requests.  For more details on how DataSource requests
// are processed by SmartClient Server, and how you can alter and add to this processing, see
// this description of +link{group:serverDataIntegration,server data integration}.
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title SQL DataSources
// @requiresModules SCServer
// @visibility external
//<

//> @groupDef adminConsole
// The Admin Console allows you to configure database access for DataSources that use
// SmartClient's built-in +link{group:sqlDataSource,SQL engine}.  You can either use the Admin
// Console UI (as explained below) or directly specify equivalent properties in your
// +link{group:server_properties,server.properties} file (see "Manually specifying.." below).
// <P>
// <smartclient>
// NOTE: To use the Admin Console, you must have the Isomorphic SmartClient package installed
// and your servlet engine started.
// <p>
// Direct your browser to the following URL to access the Admin Console:
// <p>
// &nbsp;&nbsp;+externalLink{http://localhost:8080/tools/adminConsole.jsp}
// <p>
// The common default servlet engine port 8080 is used in the URL given above. Adjust your URL
// as necessary if you are using a different port and replace localhost with the machine name
// running the servlet engine if you are accessing it from a remote machine.
// </smartclient>
// <smartgwt>
// To use the Admin Console, make sure you have followed the
// +link{group:sgwtEESetup,full installation instructions} - double check that you have the
// correct GWT &lt;inherits&gt; as shown in those instructions.  Then, launch your GWT project
// from your IDE, and open a web browser to <i>moduleBaseURL</i>/tools/adminConsole.jsp.  For
// example, if you load your application by going to a URL that ends in
// builtinds/builtinds.html, the correct URL for the Admin Console is builtinds/tools/adminConsole.jsp.
// <P>
// If it's not clear what URL to use, you can add the following code to your onModuleLoad()
// method to create a button that opens the Admin Console:
// <pre>
//   IButton adminButton = new IButton("Admin Console");
//   adminButton.addClickHandler(new ClickHandler() {
//     public void onClick(ClickEvent event) {
//       com.smartgwtee.tools.client.SCEE.openDataSourceConsole();
//     }
//   });
//   adminButton.draw();
// </pre>
// NOTE: if you are using Pro Edition, the method to call is
// com.smartgwtpro.tools.client.SCPRO.openDataSourceConsole(), and for Power Edition,
// com.smartgwtpower.tools.client.SCPower.openDataSourceConsole().
// </smartgwt>
// Having opened the Admin Console, available JNDI connections will be discovered and shown
// automatically.  If you aren't using JDNI, use the GUI to enter and test JDBC settings. Both
// ConnectionManager and JDBC DataSource settings are supported. Once you've got a working
// connection, set it as the default connection using the "Set as Default" button.
// <smartgwt>
// <P>
// These settings will be written to +link{group:server_properties,server.properties}
// in your deployment directory - use
// the "Download server.properties" button to download the settings and merge them to the
// server.properties file in your Eclipse (or other IDE) project.
// </smartgwt>
// <P>
// <b>Test Data</b>
// <p>
// You can create a test file that contains a sample dataset which can be imported into your
// database table with the Admin Console.
// <p>
// The test file to use with your DataSource is specified in the <code>testFileName</code>
// DataSource configuration property. The test file uses the extension .data.xml.
// <p>
// The test data file should consist of a top-level &lt;List&gt; element containing a series of XML
// elements named after your DataSource's ID, each of which creates one DataSource
// record. Values for each field are given within tags named after the field name.
// <p>
// For example, the following XML is from the supplyItem.data.xml test data file supplied with
// the Isomorphic SmartClient package. This file is located in
// [webroot]/examples/shared/ds/test_data/.
// <p>
// <pre>
// &lt;List&gt;
//  &lt;supplyItem&gt;
//      &lt;description&gt;A revolutionary cushion-grip ballpoint pen that reduces
//          required gripping power, relieving stress and alleviating writing
//          fatigue. Excellent for people who suffer from arthritis or carpal
//          tunnel syndrome. Medium point, black ink. Refillable.&lt;/description&gt;
//      &lt;category&gt;1&lt;/category&gt;
//      &lt;itemRef&gt;ODC 204-502-153&lt;/itemRef&gt;
//      &lt;maxQuantity&gt;5&lt;/maxQuantity&gt;
//      &lt;requiresJustification&gt;0&lt;/requiresJustification&gt;
//      &lt;itemName&gt;Dr. Grip Pens -- Blue Barrel&lt;/itemName&gt;
//      &lt;itemID&gt;1&lt;/itemID&gt;
//      &lt;unitCost&gt;4.99&lt;/unitCost&gt;
//  &lt;/supplyItem&gt;
//  &lt;supplyItem&gt;
//      &lt;description&gt;A revolutionary cushion-grip ballpoint pen that reduces
//          required gripping power, relieving stress and alleviating writing
//          fatigue. Excellent for people who suffer from arthritis or carpal
//          tunnel syndrome. Medium point, black ink. Refillable.&lt;/description&gt;
//      &lt;category&gt;1&lt;/category&gt;
//      &lt;itemRef&gt;ODC 204-708-834&lt;/itemRef&gt;
//      &lt;maxQuantity&gt;5&lt;/maxQuantity&gt;
//      &lt;requiresJustification&gt;0&lt;/requiresJustification&gt;
//      &lt;itemName&gt;Dr. Grip Pens -- Black Barrel&lt;/itemName&gt;
//      &lt;itemID&gt;2&lt;/itemID&gt;
//      &lt;unitCost&gt;4.99&lt;/unitCost&gt;
//  &lt;/supplyItem&gt;
//  &lt;supplyItem&gt;
//      &lt;description&gt;Personalized business cards for all your networking
//          needs.&lt;/description&gt;
//      &lt;category&gt;2&lt;/category&gt;
//      &lt;itemRef&gt;&lt;/itemRef&gt;
//      &lt;maxQuantity&gt;500&lt;/maxQuantity&gt;
//      &lt;requiresJustification&gt;1&lt;/requiresJustification&gt;
//      &lt;itemName&gt;Personalized business cards -- 500 count&lt;/itemName&gt;
//      &lt;itemID&gt;3&lt;/itemID&gt;
//      &lt;unitCost&gt;25.00&lt;/unitCost&gt;
//  &lt;/supplyItem&gt;
//  ...
// &lt;List/&gt;
// </pre>
//
// Data for a tree-like DataSource can be specified with the same format.
// The following code example is from the supplyCategory.data.xml test data file. This file
// is also located in [webroot]/examples/shares/ds/test_data/.
//
// <pre>
// &lt;List&gt;
//     &lt;supplyCategory&gt;
//         &lt;itemName&gt;Office Paper Products&lt;/itemName&gt;
//         &lt;parentID&gt;root&lt;/parentID&gt;
//     &lt;/supplyCategory&gt;
//     &lt;supplyCategory&gt;
//         &lt;itemName&gt;Calculator Rolls&lt;/itemName&gt;
//         &lt;parentID&gt;Office Paper Products&lt;/parentID&gt;
//     &lt;/supplyCategory&gt;
//     &lt;supplyCategory&gt;
//         &lt;itemName&gt;Adding Machine/calculator Roll&lt;/itemName&gt;
//         &lt;parentID&gt;Calculator Rolls&lt;/parentID&gt;
//     &lt;/supplyCategory&gt;
//     . . .
// &lt;/List&gt;
// </pre>
//
// Notice that all records must define values for the itemName primary key field and for the
// parentID field that establishes the tree relationship.
// <P>
// <br>
// <b>Manually specifying database connection settings</b>
// <p>
// The Admin Console maintains settings in the +link{group:server_properties,server.properties}
// file, found in
// your application's <code>WEB-INF/classes</code> directory.  If you prefer, you can maintain
// these settings by directly editing that file.  You should restart your servlet engine
// after changing this file.
// <p>
// For example, the following settings are the defaults in a new SmartClient installation for
// a MySQL server; they are approximately correct for a MySQL server running on the same
// machine as the servlet engine and listening on the default MySQL port.  For details of what
// each of these properties means, check +link{group:sqlSettings,this page}.<pre>
//   sql.Mysql.database.type: mysql
//   sql.Mysql.database.ansiMode: false
//   sql.Mysql.interface.type: dataSource
//   sql.Mysql.driver: com.mysql.jdbc.jdbc2.optional.MysqlDataSource
//   # name of the database to use
//   sql.Mysql.driver.databaseName: isomorphic
//   # hostname and port where the database server is installed
//   sql.Mysql.driver.serverName: localhost
//   sql.Mysql.driver.portNumber: 3306
//   # username and password that can create and modify tables in that database
//   # this user must have the following privileges for the system to function
//   # properly: create/alter/drop table; insert/update/replace/delete rows.
//   sql.Mysql.driver.user: root
//   sql.Mysql.driver.password:
// </pre>
// Note the distinction here between database <em>type</em> and database <em>name</em>.  Database
// type refers to the actual product - Oracle, DB2 or whatever.  In the above example, database
// type is "mysql" (all lowercase) - the value of property <code>sql.Mysql.database.type</code>.
// Database type is very important.  The type of a given database connection dictates whether
// features like SQL paging and transactions are supported; it even dictates the syntax of the
// SQL we generate.
// <p>
// Database name is just an arbitrary name for a particular database connection, and it is
// embedded in the property names immediately after the <code>sql</code> prefix. In this example
// it happens to be very similar to the database type - "Mysql" as opposed to "mysql" - but in
// fact the name has no significance and could be any string.  When referring to specific
// database connections in your +link{class:DataSource,DataSources} with the
// +link{DataSource.dbName,dbName} property, it is the database <em>name</em> you use.
// <p>
// NOTE: It is common for DataSources to not specify <code>dbName</code>.  In this case, the
// default database is used.  To specify the default database manually in
// +link{group:server_properties,server.properties}, set <code>sql.defaultDatabase</code>, using database
// name.  So, to set our example connection from above as the default:<pre>
//   sql.defaultDatabase: Mysql
// </pre>
// <P>
// <b>Manually specifying JNDI settings</b>
// <p>
// Instead of specifying database connection parameters directly in
// +link{group:server_properties,server.properties},
// it is possible to connect to a database that is configured as a JNDI resource in your
// application server.  Assume you have an Oracle JNDI resource with the name "jndiTest",
// configured similar to this in Tomcat:
// <pre>
//   &lt;Resource name="jdbc/jndiTest"
//                    auth="Container"
//                    type="javax.sql.DataSource"
//                    driverClassName="oracle.jdbc.driver.OracleDriver"
//                    url="jdbc:oracle:thin:@192.168.132.152:1521:xe"
//                    username="system"
//                    password="manager"
//                    initialSize="5"
//                    maxActive="50" /&gt;
// </pre>
// The minimal set of properties required to create a SmartClient database connection that
// attaches to this resource is as follows (Note that the <code>java:comp/env/</code> prelude
// in the first line is optional - the server will automatically look there if it can't find
// the resource in the absolute location)
// <pre>
//   sql.myOracleConnection.driver.name: java:comp/env/jdbc/jndiTest
//   sql.myOracleConnection.database.type: oracle
//   sql.myOracleConnection.interface.type: jndi
// </pre>
//
//
// @see group:sqlConnectionPooling
// @treeLocation Client Reference/Data Binding/DataSource
// @title Admin Console
// @requiresModules SCServer
// @visibility external
//<

//> @groupDef sqlSettings
// Although the Admin Console provides a UI to let you to configure database access for
// DataSources that use SmartClient's built-in +link{group:sqlDataSource,SQL engine}, it is
// also possible to configure these DataSources with manual entries in your
// +link{group:server_properties,server.properties} file.
// <p>
// When you manually configure a DataSource like this, you do so by maintaining a set of
// properties with names structured like this:
// <pre>   sql.{dbName}.x.y
// </pre>
// where <code>{dbName}</code> is the name of the database configuration you are providing.
// Note that this database name is just an arbitrary name for a particular database
// configuration; many of the default ones provided with SmartClient are named after a database
// <em>type</em>, in order to make their intended use more immediately obvious, but this is
// not by any means a requirement.
// <p>
// For the remainder of this discussion, we will assume we are configuring a database with a
// name of "MyDatabase".
// <p>
// <h4>SQL configuration properties</h4><p>
// <b><code>sql.MyDatabase.database.type</code></b><br>
// This should be set to one of the supported database types.  These are:<p>
// <table style="font-size:10px">
// <tr><td><b>hsqldb</b></td><td>HSQLDB 1.7.x and greater</td></tr>
// <tr><td><b>db2</b></td><td>IBM DB2 8.x and greater</td></tr>
// <tr><td><b>db2iSeries</b></td><td>IBM DB2 for iSeries/i5, V5R4 and greater</td></tr>
// <tr><td><b>firebirdsql</b></td><td>Firebird 2.5 and greater</td></tr>
// <tr><td><b>informix</b></td><td>Informix 11.5 and greater</td></tr>
// <tr><td><b>sqlserver</b></td><td>Microsoft SQL Server 2000 and greater</td></tr>
// <tr><td><b>mysql</b></td><td>MySQL 3.2.x and greater</td></tr>
// <tr><td><b>oracle</b></td><td>Oracle 8.0.5, 8i and greater</td></tr>
// <tr><td><b>postgresql</b></td><td>PostgreSQL 7.x and greater</td></tr>
// <tr><td><b>generic</b></td><td>A generic SQL92 database, with limitations described in
// +link{group:sqlDataSource,this article}</td></tr>
// </table><p>
// <code><b>sql.MyDatabase.driver</b></code><br>
// The name of the JDBC driver implementation.  This depends upon your database product and
// version, and the specific JDBC driver you are using (JDBC drivers can usually be downloaded
// from your database vendor's website).  Bearing in mind the caveat that this information can
// vary by release and JDBC implementation, here are some suggested values for our supported
// databases:<p>
// <table style="font-size:10px">
// <tr><td><b>hsqldb</b></td><td><code>org.hsqldb.jdbcDriver</code></td></tr>
// <tr><td><b>db2</b></td><td><code>com.ibm.db2.jcc.DB2DataSource</code></td></tr>
// <tr><td><b>db2iSeries</b></td><td><code>com.ibm.as400.access.AS400JDBCDriver</code></td></tr>
// <tr><td><b>firebirdsql</b></td><td><code>org.firebirdsql.jdbc.FBDriver</code></td></tr>
// <tr><td><b>informix</b></td><td><code>com.informix.jdbc.IfxDriver</code></td></tr>
// <tr><td><b>sqlserver</b></td><td><code>com.microsoft.jdbc.sqlserver.SQLServerDriver</code> or
// <code>com.microsoft.sqlserver.jdbc.SQLServerDriver</code> (Microsoft changed the order of
// "jdbc" and "sqlserver" between the 2000 and 2005 editions of the product)</td></tr>
// <tr><td><b>mysql</b></td><td><code>com.mysql.jdbc.jdbc2.optional.MysqlDataSource</code></td></tr>
// <tr><td><b>oracle</b></td><td><code>oracle.jdbc.driver.OracleDriver</code></td></tr>
// <tr><td><b>postgresql</b></td><td><code>org.postgresql.Driver</code></td></tr>
// </table><p>
// <code><b>sql.MyDatabase.driver.serverName</b></code><br>
// The name or IP address of the database server
// <p>
// <code><b>sql.MyDatabase.driver.portNumber</b></code><br>
// The port on which the database server is listening
// <p>
// <code><b>sql.MyDatabase.driver.user</b></code><br>
// The user to connect as
// <p>
// <code><b>sql.MyDatabase.driver.password</b></code><br>
// The user's password
// <p>
// <code><b>sql.MyDatabase.driver.databaseName</b></code><br>
// The database to connect to.  A "database" in this context is a named collection of tables
// and other database resources that are somehow grouped together by the database product.
// The specifics of how this is implemented vary by database.  Note that some database
// products use the terms "catalog" or "schema" to refer to the same concept, and Oracle -
// although it does also have a concept of catalog - uses the term "SID" for this concept.<p>
// <code><b>sql.MyDatabase.interface.type</b></code><br>
// Indicates how the JDBC connection will be created or looked up; the value of this setting
// depends on the capabilities of the particular JDBC driver you are using, and is inherently
// connected to the value of <code>sql.MyDatabase.driver</code>.  The following settings are
// supported:<p>
// <b>dataSource</b> - the driver is an instance of <code>javax.sql.DataSource</code> and
// should be instantiated by SmartClient Server<br>
// <b>driverManager</b> - the driver is an instance of <code>java.sql.DriverManager</code><br>
// <b>jndi</b> - the driver is an instance of <code>javax.sql.DataSource</code> and should be
// looked up using JNDI<p>
// <code><b>sql.MyDatabase.driver.url</b></code><br>
// For configurations where <code>sql.MyDatabase.interface.type</code> is "driverManager",
// this property allows you to manually enter the URL we use to connect to the database.  If
// this property is not provided, we build the URL from other settings such as
// <code>sql.MyDatabase.driver.serverName</code> and
// <code>sql.MyDatabase.driver.databaseName</code>.<p>
//
// <b>Other properties</b><br>
// Different JDBC drivers support different properties to support product-specific quirks and
// features.  You can often specify these properties by embedding them as parameters in the
// URL used to connect to the database.<p>
//
// Alternatively, any subproperty you set on the "driver" in server.properties is applied to
// the JDBC driver object via Reflection. For example, the MySQL JDBC driver supports a property
// "useUnicode", which forces the database to use Unicode character encoding.  If
// <code>sql.MyDatabase.driver</code> is <code>com.mysql.jdbc.jdbc2.optional.MysqlDataSource</code>,
// setting <code>sql.MyDatabase.driver.useUnicode</code> to true means we'll attempt to call
// <code>setUseUnicode(true)</code> on this class.  This would have exactly the same effect as
// defining the connection URL manually and specifying the parameter <code>useUnicode=true</code>
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title SQL Database Settings in <code>server.properties</code>
// @requiresModules SCServer
// @visibility external
//<

//> @groupDef clientOnlyDataSources
// For prototyping purposes, a "client-only" DataSource can be created that has no permanent
// storage and never contacts the server, instead using a set of test data to respond to
// requests in the same manner as a server-based DataSource might.
// <P>
// The client-side interface to a client-only DataSource is identical to a server-based
// DataSource, including asynchronous responses, so that a client-only DataSource can be
// replaced by a server-based DataSource without code changes.  The only difference is that
// changes to records belonging to the DataSource persist only until the page is reloaded.
// <p>
// Client-only DataSources allow you to create a complete prototype application in an .html
// file that does not require a server.
// <p>
// <smartclient>
// The <code>clientOnly</code> property is specified to create a client-only DataSource, and
// the <code>testData</code> property should contain the test dataset, as an Array of Objects,
// one per DataSource record.  For example:
// <pre>
//   isc.DataSource.create({
//       ID:"supplyItem",
//       fields: ...,
//       clientOnly:true,
//       testData:[
//          {itemName:"Pencil", cost:5.50},
//          ...
//       ]
//   });
// </pre>
// If you have existing test data in XML (see the +link{group:adminConsole,Admin Console} for
// expected format),
// you can use the XML->JS translation engine to load it into a client-only DataSource, like so:
// <pre>
//   isc.DataSource.create({
//     ID:"solutions",
//     fields: ...,
//     clientOnly : true,
//     testData :
//         &lt;isomorphic:XML filename="shared/ds/test_data/solutions.data.xml"/&gt;
//   });
// </pre>
// Another useful practice is to specify both the clientOnly DataSource and its test data in
// XML, so that the +link{group:adminConsole,Admin Console} can later be used to import the
// DataSource and its test data into a SQL Database.  An idiom for accomplishing this is:
// <pre>
//   &lt;isomorphic:loadDS name="solutions"/&gt;
//   isc.DataSource.getDataSource("solutions").addProperties({
//     clientOnly : true,
//     testData :
//        &lt;isomorphic:XML filename="shared/ds/test_data/solutions.data.xml"/&gt;
//   });
// </pre>
// If you specify your DataSource as <code>clientOnly: true</code>, omit testData
// entirely, and provide either a +link{attr:dataSource.dataURL} or a <code>testFileName</code>, the
// </smartclient>
// <smartgwt>
// The setClientOnly method is specified to create a client-only DataSource. For example:
// <pre>
//  DataSource dataSource = new DataSource();
//  dataSource.setID("stockQuotesDS");
//  DataSourceField idField = new DataSourceField("id", FieldType.INTEGER, "Id");
//  ........
//  dataSource.setFields(idField,.....);
//  dataSource.setClientOnly(true);
// </pre>
// If you have existing test data in XML (see the Admin Console for expected format),
// you can load it into a client-only DataSource using setDataURL method, like so:
// <pre>
//  DataSource dataSource = new DataSource();
//  dataSource.setID("stockQuotesDS");
//  dataSource.setRecordXPath("/List/stockQuotes");
//  DataSourceField idField = new DataSourceField("id", FieldType.INTEGER, "Id");
//  ........
//  dataSource.setFields(idField,.....);
//  dataSource.setDataURL("ds/test_data/stockQuotes.data.xml");
//  dataSource.setClientOnly(true);
// </pre>
// The
// </smartgwt>
// DataSource will lazily make a one-time fetch against the specified data file the first time
// an operation is called on it.  From then on, the DataSource will work against the local
// cache created from this initial request.  This is a quick way to prototype against some test
// data that may eventually be returned from an arbitrary back-end.
// <P>
// Finally, it is possible to have a DataSource which initially fetches the entire dataset and
// performs all subsequent fetching locally, while still visiting the server to perform all
// other operations.  See +link{dataSource.cacheAllData}.
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title Client Only DataSources
// @visibility external
//<


// synonym, isc namespace only (never a global)
isc.DS = isc.DataSource;

// static properties and methods
isc.DataSource.addClassProperties({


    // locating dataSources
    dataSourceObjectSuffix : "DS",            // suffix for the global to hold info about a dataSource,
                                            //    eg:  var myDataSourceDS = {...}

    _dataSources : {},                        // internal array to hold the list of dataSources we've loaded
    _elements : {},                            // internal array of XSElements
    _types : {},                            // internal array of XSComplexTypes
    _$element : "element", _$type:"type",
    _tableCodesToDatasourceIds: {},         // a map to find loaded sql datasource by it's tableCode
    _fkTableCodesToDatasourceIds: {},

    TABLE : "table",                        // table type of datasource
    VIEW : "view",                            // view type of datasource (virtual datasource)

    //> @classAttr DataSource.loaderURL (URL : DataSource.loaderURL : RW)
    //
    // The URL where the DataSourceLoader servlet has been installed.  Defaults to the
    // +link{Page.setIsomorphicDir,isomorphicDir} plus "/DataSourceLoader".  Change by calling
    // calling +link{DataSource.setLoaderURL()}
    //
    // @visibility external
    //<
    loaderURL:"[ISOMORPHIC]/DataSourceLoader",

    // deepCloneOnEdit: If we have nested dataSources, and have edit-components bound to these
    // DS's, using dataPath to extract values from inner objects, should the field values be
    // recursively duplicated on edit.
    // Default value is true - this is required to support standard functionality such as being
    // able to pass a ListGrid record to a DynamicForm for editing and avoiding having both
    // components referencing the same inner-objects in memory [thus leading to edits directly
    // effecting the ListGrid record with no save-step or other notification].

    deepCloneOnEdit:true,

    // See class method registerSgwtDS()
    DS_FN: {},

    // chunks of SOAP messages
    _soapEnvelopeStart : "<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' ",
    _soapEnvelopeEnd : "</soap:Envelope>",
    _soapHeaderStart : "<soap:Header>",
    _soapHeaderEnd : "</soap:Header>",
    _soapBodyStart : "<soap:Body", // intentionally unterminated
    _soapBodyEnd : "</soap:Body>"
});

isc.DataSource.addClassMethods({

    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {
        var id = A ? A.ID : null;
        if (isc.DataSource._doNotClobber) {
            var instance;
            if (id) instance = isc.DataSource.get(id);
            if (instance) return instance;
        }

        if (this.DS_FN) delete this.DS_FN[id];

        return this.invokeSuper(this, "create", A,B,C,D,E,F,G,H,I,J,K,L,M);
    },

    // Looking up and loading DataSources
    // --------------------------------------------------------------------------------------------

    isLoaded : function (name) {
        // if no name was passed to us, return false
        if (!name) return false;
        if (isc.isA.DataSource(name) || this._dataSources[name]) return true;
        return false;
    },

    //> @classMethod DataSource.getDataSource()
    // Lookup a DataSource by ID.
    //
    // @param ID (identifier) DataSource ID
    // @return (DataSource) the DataSource with this ID, if loaded, otherwise null.
    // @visibility external
    //<
    // If the dataSource does not exist locally and a callback is provided to this method, we
    // attempt to fetch the named datasource from the server and callback.
    _$refColon:"ref:",
    getDataSource : function (name, callback, context, schemaType) {
        // if no name was passed to us, return null
        if (!name) return null;

//        if (isc.isA.DataSource(name)) {
//            alert("getting datasource: " + name);
//        } else {
//            alert("getting name: " + name);
//        }

        // handle being passed a DataSource instance
        if (isc.isA.DataSource(name)) return name;

        // strip off the "ref:" prefix used in the XML format.  Happens when a field of
        // DataSource type has been declared as an XML attribute (if it's not an attribute you
        // get <DataSource ref="dsId"/> and this is handled during XML->JS translation)
        if (isc.startsWith(name, this._$refColon)) {
            name = name.substring(4);
        }

        if (schemaType && isc.WebService) {
            if (schemaType == isc.DS._$element) return this._elements[name];
            if (schemaType == isc.DS._$type) return this._types[name];
            return null;
        }

        // load the ds (clientside)
        var ds = this._dataSources[name];
        if (!ds) {
            // If this is a DataSource that has been registered as having been created in
            // SmartGWT, create it JS-side now
            if (isc.DataSource.DS_FN && isc.DataSource.DS_FN[name]) {
                // Remove it from the SGWT pending list before calling into the create
                // method, otherwise we get a recursion loop
                var work = isc.DataSource.DS_FN[name];
                delete isc.DataSource.DS_FN[name];
                work.call(null);
            }
            ds = this._dataSources[name];
            if (!ds) {
                ds = this._loadDataSource(name, callback);
                if (ds) ds.ID = name; // tell the dataSource its name for later
            }
        }

        if (ds) {
            // if they've specified a callback, call the callback in addition to returning the ds
            if (callback) {
                this.fireCallback(callback, "ds", [ds], ds);
            }

            return ds;
        }

        // load from server if a callback was passed
        if (callback) {
            this.loadSchema(name, callback, context);
        }

        // let the caller know that we don't have the datasource.  if a callback was passed,
        // the caller will be notified via the callback.
        return null;
    },

    // loadSchema - attempt to load a remote dataSource schema from the server.
    // This is supported as part of the SmartClient server functionality
    loadSchema : function (name, callback, context) {
        this.logWarn("Attempt to load schema for DataSource '" + name +
            "'. This dataSource cannot be found. To load DataSources from the server without " +
            "explicit inclusion in your application requires optional SmartClient server " +
            "support - not present in this build.");
        return null;
    },


    //> @classMethod DataSource.get()
    // Synonym of +link{getDataSource()}: Lookup a DataSource by ID.
    //
    // @param ID (identifier) DataSource ID
    // @return (DataSource) the DataSource with this ID, if loaded, otherwise null.
    // @visibility external
    //<
    get : function (name, callback, context, schemaType) {
        return this.getDataSource(name, callback, context, schemaType);
    },

    _loadDataSource : function (name, callback) {
        if (callback) return null; // no error if we're going to try to load from server.

        //>DEBUG
        if (name != isc.auto && this.logIsDebugEnabled()) {
            this.logDebug("isc.DataSource '" + name + "' not present");
        }
        //<DEBUG

        // couldn't load the dataSource -- return null
        return null;
    },

    // returns a list of all registered datasources
    getRegisteredDataSources : function () {
        return isc.getKeys(this._dataSources);
    },

    // returns true if the named DS is registered, false otherwise
    isRegistered : function (name) {
        if (this._dataSources[name]) return true;
        return false;
    },


    // Foreign Key relationships
    // --------------------------------------------------------------------------------------------
    // field.foreignKey can be [dataSourceID].[fieldName] or just [dataSourceID], where the latter
    // form means it's a foreignKey on the other dataSource's primary key fields.

    // methods to break up field.foreignKey into the target DataSource and field name.  Factored in
    // case we ever want to change our somewhat hokey foreign key scheme
    getForeignFieldName : function (field) {
        var foreignKey = field.foreignKey,
            dotIndex = foreignKey.indexOf(".");

        // If there is no dot the foreignkey is a field within this dataSource.

        if (dotIndex == -1) return foreignKey;
        return foreignKey.substring(dotIndex + 1);
    },

    getForeignDSName : function (field, defaultDS) {
        var foreignKey = field.foreignKey,
            dotIndex = foreignKey.indexOf(".");

        // If there is no dot the foreignkey is a field within this dataSource.

        if (dotIndex == -1) {
            if (defaultDS == null) return null;
            return isc.isA.String(defaultDS) ? defaultDS : defaultDS.ID;
        }
        return foreignKey.substring(0, dotIndex);
    },

    registerDataSource : function (dataSource) {
        //>DEBUG
        if (this.logIsInfoEnabled()) {
            this.logInfo("Registered new isc.DataSource '" + dataSource.ID + "'");
        }
        //<DEBUG
        if (dataSource.ID) {
            var existingDS = this._dataSources[dataSource.ID];
            // overwrite any existing registered DataSource, unless we have a schemaNamespace.
            // This avoids XMLSchema-derived Schema cloberring ordinary DataSources, since
            // XMLSchema-derived DataSources can be looked up via other means
            // (SchemaSet.getSchema())
            if (!existingDS || !dataSource.schemaNamespace) {
                this._dataSources[dataSource.ID] = dataSource;
            }
            if (dataSource.tableCode) {
                this._tableCodesToDatasourceIds[dataSource.tableCode] = dataSource.ID;
                var linkedDatasources = this._fkTableCodesToDatasourceIds[dataSource.tableCode];
                if (linkedDatasources) {
                    for (var i = 0; i < linkedDatasources.length; i++) {
                        var linkedDSName = linkedDatasources[i];
                        var linkedDS = isc.DS.get(linkedDSName);
                        var linkedDSFields = linkedDS.getFields();
                        var dsFields = dataSource.getFields();
                        for (var linkedDSFieldName in linkedDSFields) {
                            for (var dsFieldName in dsFields) {
                                if (linkedDSFields[linkedDSFieldName].fkColumnCode == dsFields[dsFieldName].columnCode) {
                                    linkedDSFields[linkedDSFieldName].foreignKey = dataSource.ID + "." + dsFields[dsFieldName].name;
                                    break;
                                }
                            }
                        }
                        linkedDS.addChildDataSource(dataSource);
                        linkedDatasources.removeAt(i);
                        i--;
                    }
                    if (linkedDatasources.length == 0) {
                        delete this._fkTableCodesToDatasourceIds[dataSource.tableCode];
                    }
                }
            }
        }


        if (isc.Schema && isc.isA.Schema(dataSource)) {
            if (isc.isAn.XSElement(dataSource)) this._elements[dataSource.ID] = dataSource;
            else if (isc.isAn.XSComplexType(dataSource)) this._types[dataSource.ID] = dataSource;
            return;
        }



        // link DataSources with foreignKeys to each other as they are loaded
        // -----------------------------------------------------------------------------------

        var fields = dataSource.getLocalFields(true);

        var unlinkedChildren = this._unlinkedChildren = (this._unlinkedChildren || {});

        // try to satisfy this DS's foreignKey relationships
        for (var fieldName in fields) {
            var field = fields[fieldName];
            var targetDSName = null;
            if (field.fkTableCode) {
                targetDSName = this._tableCodesToDatasourceIds[field.fkTableCode];
                // check whether sql datasource with encoded tableName was already loaded
                if (targetDSName == null) {
                    if (this._fkTableCodesToDatasourceIds[field.fkTableCode] == null) {
                        this._fkTableCodesToDatasourceIds[field.fkTableCode] = [];
                    }
                    this._fkTableCodesToDatasourceIds[field.fkTableCode].add(dataSource.ID);
                    continue;
                }
                var ds = isc.DS.get(targetDSName);
                var dsFields = ds.getFields();
                for (var dsFieldName in dsFields) {
                    if (dsFields[dsFieldName].columnCode == field.fkColumnCode) {
                        field.foreignKey = ds.ID + "." + dsFields[dsFieldName].name;
                    }
                }
            } else if (field.foreignKey) {
                var targetDSName = this.getForeignDSName(field, dataSource);
            } else {
                continue;
            }
            if (isc.DS.isRegistered(targetDSName)) {
                //this.logWarn("dataSource: " + dataSource.ID + " found parent: " + targetDS.ID);
                // tell the other DataSource that this DS links to it in a 1 to many
                // relationship
                isc.DS.get(targetDSName).addChildDataSource(dataSource);
            } else {
                //this.logWarn("dataSource: " + dataSource.ID + " could not find parent: " +
                //             targetDSName);
                // add ourselves to the list of future children of targetDS, to be linked up
                // whenever it gets loaded
                if (unlinkedChildren[targetDSName] == null) {
                    unlinkedChildren[targetDSName] = [];
                }
                unlinkedChildren[targetDSName].add(dataSource);
            }
        }

        // see if this new DS satisfies any foreignKey relationships
        var children = unlinkedChildren[dataSource.ID];
        if (children != null) {
            //this.logWarn("dataSource: " + dataSource.ID + " found children: " + children);
            dataSource.map("addChildDataSource", children);
            unlinkedChildren[dataSource.ID] = null;
        }

        // Link DataSources that declare childRelations to their children
        // ---------------------------------------------------------------------------------
        var unfoundChildren = this._unfoundChildren = this._unfoundChildren || {};
        if (dataSource.childRelations) {
            for (var i = 0; i < dataSource.childRelations.length; i++) {
                var relation = dataSource.childRelations[i],
                    childDSName = relation.dsName,
                    childDS = isc.DS.get(childDSName);
                if (childDS) {
                    //this.logWarn("dataSource: " + dataSource.ID +
                    //             " found child: " + childDS.ID);
                    this._addChildRelation(dataSource, childDS, relation);
                } else {
                    //this.logWarn("dataSource: " + dataSource.ID +
                    //             " could not find child: " + childDSName);
                    if (unfoundChildren[childDSName] == null) {
                        unfoundChildren[childDSName] = [];
                    }
                    relation.parentDS = dataSource.ID;
                    unfoundChildren[childDSName].add(relation);
                }
            }
        }
        // eg User loaded, now Account is loaded
        var relations = unfoundChildren[dataSource.ID];
        if (relations) {
            for (var i = 0; i < relations.length; i++) {
                var relation = relations[i];
                this._addChildRelation(isc.DS.get(relation.parentDS), dataSource, relation);
            }
        }

    },

    _addChildRelation : function (parentDS, childDS, relation) {
        parentDS.addChildDataSource(childDS);

        // create foreignKey marker on field
        if (!relation.fieldName) return;

        var field = childDS.getField(relation.fieldName);
        if (!field.foriegnKey) {
            // HACK allows getTreeRelationship to work
            field.foreignKey = parentDS.ID + "." +
                                    parentDS.getPrimaryKeyFieldNames()[0];
        }
    },

    // SmartGWT DataSource registration
    // ---------------------------------------------------------------------------------------

    registerSgwtDS : function (id, commandObject) {
        isc.DataSource.DS_FN[id] = commandObject;
    },
    unregisterSgwtDS : function (id) {
        delete isc.DataSource.DS_FN[id];
    },
    reregisterSgwtDS : function (oldId, newId) {
        isc.DataSource.DS_FN[newId] = isc.DataSource.DS_FN[oldId];
        delete isc.DataSource.DS_FN[oldId];
    },

    // Schema and types
    // ---------------------------------------------------------------------------------------

    // Simple helper to get a field value for a record, respecting dataPath and SimpleType.
    //> @classMethod DataSource.getFieldValue()
    // Given a field definition and a record object, this method
    // will return the field value for the record.
    // <P>
    // This method will follow any +link{listGridField.dataPath,dataPath} specified on the
    // component field if necessary, and will extract
    // +link{simpleType.getAtomicValue(),atomic values} from custom +link{SimpleType} fields
    // where this is required.
    // @param field (DataSourceField | ListGridField | DetailViewerField | FormItem)
    //  Field definition from a dataSource or dataBoundComponent.
    // @param record (Record) data object
    // @return (any) extracted field value
    // @visibility external
    //<
    getFieldValue : function (field, record) {
        return isc.Canvas._getFieldValue(null, field, record);
    },

    // helper method that returns a shallow copy of requestProperties
    // we want to avoid side-effecting the requestProperties that application developers
    // pass into our APIs, since otherwise re-used requestProperties objects might carry over
    // settings like requestProperties.prompt or requestProperties.operationId between different
    // types of operations.
    dupRequest : function (requestProperties) {
        if (!requestProperties || requestProperties._alreadyDuped) return requestProperties;

        return isc.addProperties({_alreadyDuped:true}, requestProperties);
    },

    // helper method for getObjectField; returns inheritance distance or -1 on error
    getInheritanceDistance : function (superclass, subclass) {
        // check that the arguments are in fact classes, and confirm their relationship
        var superclassObj = isc.ClassFactory.getClass(superclass),
            subclassObj = isc.ClassFactory.getClass(subclass)
        ;
        if (superclassObj == null || subclassObj == null) {
            this.logWarn("Invalid superclass and/or subclass argument provided");
            return -1;
        }
        if (!subclassObj.isA(superclass)) {
            this.logWarn(subclass + " is not a subclass of " + superclass);
            return -1;
        }

        // The normal procedure works if both are Class or both are
        // SGWTFactory.  In the mixed case, we need to get the scClass for the
        // SGWTFactory and compute using that.
        if (isc.isA.SGWTFactoryObject(superclassObj)) {
            if (!isc.isA.SGWTFactoryObject(subclassObj)) {
                superclassObj = superclassObj.getDefaultScClass();
                if (!superclassObj) {
                    this.logWarn("Could not get defaultScClass for SGWTFactory " + superclass);
                    return -1;
                }
            }
        } else if (isc.isA.SGWTFactoryObject(subclassObj)) {
            // We only get here if the superclass is not an SGWTFactory
            subclassObj = subclassObj.getDefaultScClass();
            if (!subclassObj) {
                this.logWarn("Could not get defaultScClass for SGWTFactory " + subclass);
                return -1;
            }
        }

        for (var distance = 0; subclassObj != superclassObj; distance++) {
            subclassObj = subclassObj.getSuperClass();
        }

        return distance;
    },

    // simple: String, Number, Boolean, Date, Time, Function
    // non-simple: null, Object, Array

    isSimpleTypeValue : function (value) {
        if (value != null && (!isc.isAn.Object(value) || isc.isA.Date(value))) return true;
        return false;
    },

    // Given an object, find the nearest schema to be used to serialize it.
    // If there is a SmartClient Class, but no schema exists (eg custom class), we find the
    // nearest SmartClient super class that has a schema.
    getNearestSchema : function (object) {
        if (object == null) return null;

        var className;
        if (isc.isA.String(object)) className = object;
        else {
            className = isc.isAn.Instance(object) ? object.getClassName() :
                                                    object._constructor || object.type || object.$schemaId;
        }

        var schema = isc.DS.get(className);

        // if no schema is loaded for this class, look for a superclass schema
        var theClass = isc.ClassFactory.getClass(className);
        var originalClass = theClass;

        if (theClass != null) {
            var lastSuperClass = null;
            while (schema == null && (theClass = theClass.getSuperClass()) != null &&
                                      theClass != lastSuperClass)
            {
                schema = isc.DS.get(theClass.getClassName());
                lastSuperClass = theClass;
            }

            // If we have no schema, and we're looking at an SGWTFactoryObject,
            // then try again with the SC equivalent
            if (!schema) {
                if (isc.isA.SGWTFactoryObject(originalClass)) {
                    var scClassName = originalClass.getDefaultScClassName();
                    if (scClassName) return this.getNearestSchema(scClassName);
                }
            }
        }

        return schema || isc.DS.get("Object");
    },

    // Given a SmartClient component schema name, determine the associated SmartClient class

    getNearestSchemaClass : function (type) {
        if (type == null) return null;
        var theClass;

        while (theClass == null) {
            // Pick up the dataSource for the className passed in.
            // This will allow us to determine the correct SmartClient class for the object
            // by iterating up the datasource parent chain if necessary
            var schema = isc.DS.get(type);
            // Catch the case where we're passed a schema string that we don't recognize
            if (schema == null) {
                // Check if it represents an SGWT class ... in that case, use the SGWTFactory
                var sgwtClass = isc.ClassFactory.getClass(type);
                if (sgwtClass && isc.isA.SGWTFactoryObject(sgwtClass)) {
                    return sgwtClass;
                } else {
                    return null;
                }
            }
            theClass = isc.ClassFactory.getClass(schema._constructor || schema.Constructor
                                                                     || schema.type);
            //this.logWarn("schema is: " + schema + ", theClass: " + theClass +
            //             ", schema.inheritsFrom: " + schema.inheritsFrom);

            if (theClass != null) return theClass;
            type = schema.inheritsFrom;

            if (!type) return null;
        }
        return null;
    },

    // Standard Operations
    // ----------------------------------------------------------------------------------------

    // Centrally handle backcompat synonyms for operation types
    _getStandardOperationType : function (operationType) {
        switch (operationType) {
            case "fetch":
            case "select":
            case "filter":
                return "fetch";
            case "add":
            case "insert":
                return "add";
            case "update":
                return "update";
            case "remove":
            case "delete":
                return "remove";

            default:
                // cases not covered:
                // "replace": no longer supported, but not equivalent to any supported type
                // "validate": never had a synonym

            return operationType;
        }
    },

    isClientOnly : function (dataSource) {
        if (isc.isA.String(dataSource)) dataSource = this.getDataSource(dataSource);
        if (!dataSource) return false;
        return dataSource.clientOnly;
    },

    // ResultSets creation
    // --------------------------------------------------------------------------------------------

    // make a default operation for the specified dataSource, using the specified application.
    // if no application is specified, use the default application.
    makeDefaultOperation : function (dataSourceId, operationType, operationId)
    {
        // XXX this is really only intended for the standard set of operations (fetch, update,
        // add, remove): check that operationType is valid?

        var theApp = isc.rpc.app();

        if (isc.isA.DataSource(dataSourceId)) dataSourceId = dataSourceId.ID;
        if (!dataSourceId) {
            dataSourceId = "auto";
        } else if (operationId) {

            // If we have a valid dataSource and operationId, store/lookup against the
            // dataSource, not the app - so, eg, we can have an operationId "customFetch"
            // on more than one dataSource
            var dataSource = isc.DataSource.get(dataSourceId);
            if (isc.isA.DataSource(dataSource)) {
                if (!dataSource.createdOperations) dataSource.createdOperations = {};
                var operation = dataSource.createdOperations[operationId];
                if (operation == null) {
                    operation = {ID:operationId,
                                 dataSource:dataSourceId,
                                 type:operationType,
                                 filterType:"paged",
                                 loadDataOnDemand:true};
                    dataSource.createdOperations[operationId] = operation;
                }
                return operation;
            }
        }

        // NOTE: removed code that added the datasource to the application if it wasn't
        // already there so that it could be looked up when performing the operation;
        // now behavior is that if no datasources are defined on the app, it will just
        // look for a currently loaded datasource with that name

        // make sure the app has an operations array
        if (theApp.operations == null) theApp.operations = {};

        // create the default operation on this DataSource

        // allow an operationId, otherwise create one based on DS and operation type
        operationId = operationId || dataSourceId + "_" + operationType;
        var operation = theApp.operations[operationId];
        if (operation == null) {
            operation = {ID:operationId,
                         dataSource:dataSourceId,
                         type:operationType,
                         filterType:"paged",
                         loadDataOnDemand:true,
                         source:"auto"};
            theApp.operations[operationId] = operation;


        }

        // return the operation
        return operation;
    },

    // ResultSet cache sync
    // --------------------------------------------------------------------------------------------
    // NOTE: if an operation modifies more than one DataSource, obviously its not going to make
    // sense to pass the same updateData to ResultSets on different DataSources.  We assume you
    // know that you don't have a ResultSet on the second DataSource, or are dropping cache on
    // updates!

    handleUpdate : function (dsResponse, dsRequest) {
        if (!this.isUpdateOperation(dsRequest.operationType)) return;

        // notify the datasource that the operation affects
        var ds = this.get(dsRequest.dataSource);
        ds.updateCaches(dsResponse, dsRequest);
    },

    // is this an operation which alters data?
    isUpdateOperation : function (operation) {
        if (operation == "add" || operation == "update" || operation == "remove" ||
            operation == "replace"
            //>!BackCompat 2004.7.23 synonyms
            || operation == "delete" || operation == "insert"
            //<!BackCompat
           ) return true;
    },

    getUpdatedData : function (dsRequest, dsResponse, useDataFromRequest) {
        var ds = this.get(dsRequest.dataSource);
        return ds.getUpdatedData(dsRequest, dsResponse, useDataFromRequest);
    },

    // ---------------------------------------------------------------------------------------

    // given a set of values from a DynamicForm, return appropriate filter criteria
    filterCriteriaForFormValues : function (formValues) {
        if (isc.DS.isAdvancedCriteria(formValues)) return formValues;

        var filterValues = {};
        for (var fieldName in formValues) {
            var formValue = formValues[fieldName];
            // Form fields may have the value "" if they held a value and were cleared out.  A
            // blank field in a form used as a filter means "field can have any value", not
            // "field must be blank", so we eliminate blank field->value pairs.
            // This also goes for a selectList where a blank value is selected, which is
            // supposed to mean "Any".
            if (formValue == null || isc.is.emptyString(formValue)) continue;

            // multi-selects are returned as an array.
            if (isc.isAn.Array(formValue)) {

                // If nothing is selected we get [], meaning no criteria
                if (formValue.length == 0) continue;
                // If blank is selected, we get ["", "otherValue", ...], meaning "field can
                // have any value".

                for (var i = 0; i < formValue.length; i++) {
                    var orValue = formValue[i];
                    if (isc.isAn.emptyString(orValue)) continue;
                }
            }

            // use the formValue as is
            filterValues[fieldName] = formValue;

            // NOTE: we do this on the client rather than the server because otherwise the client
            // would be unable to express "field must be blank" - the server would discard the
            // criterion assuming it was an artifact of the filterValues having come from a
            // DynamicForm.
        }
        return filterValues;
    },

    checkEmptyCriteria : function (criteria, subCriterion) {

        if ((subCriterion || this.isAdvancedCriteria(criteria)) && criteria.criteria) {
            if (criteria.criteria.length == 0 ) return null;

            for (var i=criteria.criteria.length-1; i>=0; i--) {
                var subCrit = criteria.criteria[i],
                    shouldRemove = false;

                if (!subCrit) shouldRemove = true;
                else {
                    if (!subCrit.criteria) {
                        // is this an empty object?
                        if (isc.isA.emptyObject(subCrit)) shouldRemove = true;
                    } else {
                        var crit = this.checkEmptyCriteria(subCrit, true);
                        if (crit) criteria.criteria[i] = crit;
                        else shouldRemove = true;
                    }
                }

                if (shouldRemove) criteria.criteria.removeAt(i);
            }
        }

        return criteria
    },

    //> @classAttr DataSource.serializeTimeAsDatetime (boolean : false : IRA)
    // Specifies how time field values should be serialized when being sent to the server for
    // dataSources with dataFormat <code>"xml"</code> or <code>"json"</code>.
    // If <code>false</code> the time field will be serialized as a logical time object in
    // UTC, using the standard truncated XML Schema format: <code>"HH:MM:SS"</code>.
    // If <code>true</code> the time field will be serialized as a complete dateTime object
    // matching the value of the underlying JavaScript date object representing this time on the
    // client.
    // @visibility external
    //<

    serializeTimeAsDatetime:false,

    //> @classMethod DataSource.load()
    // Load a DataSource or an array of DataSources using the DataSourceLoader servlet.  When
    // a callback is specified, this is fired after the DataSources are loaded.  The callback
    // is passed a single parameter, the <code>dsID</code> list passed into the method.  If
    // no loading occurs because the requested DataSource(s) are already loaded, a warning is
    // logged and the callback is fired immediately.
    // <P>
    // To force reloading of DataSources that have already been loaded,
    // pass <code>true</code> for the forceReload parameter.
    //
    // @param dsID (String | Array of String) DataSource ID or Array of DataSource IDs
    // @param callback (Function) Callback to fire after DataSource loading completes
    // @param [forceReload] (boolean) Forcibly reload a dataSource if it's already loaded
    // @visibility external
    //<
    load : function (dsID, callback, forceReload, loadParents) {
        if (!isc.isAn.Array(dsID)) dsID = [dsID];

        if (dsID.length <= 0) {
            this.logWarn("No DataSource IDs passed in.");
            return;
        }

        var loadThese = [];

        for (var i=0; i<dsID.length; i++) {
            if (!this.isLoaded(dsID[i]) || forceReload) loadThese.add(dsID[i]);
        }

        var dsList = loadThese.join(","),
            url = isc.DataSource.loaderURL +
                    (isc.DataSource.loaderURL.contains("?") ? "&" : "?") +
                    (loadParents ? "loadParents=true&" : "") + "dataSource="+dsList,
            _dsID = dsID;
        ;

        if (loadThese.length > 0) {
            // post out to the DataSourceLoader servlet
            isc.RPCManager.send(null,
                function (rpcResponse, data, rpcRequest) {
                    //!OBFUSCATEOK
                    if (rpcResponse.httpResponseCode == 404) {
                        isc.warn("The DataSourceLoader servlet is not installed.");
                        return null;
                    }
                    if (loadParents && !forceReload) isc.DataSource._doNotClobber = true;
                    try {
                        // eval the returned create() statement(s) and fire the callback if passed
                        eval(data);
                    } catch(e) {
                        isc.logWarn("Encountered problems trying to load DataSources: " +
                                            isc.echoAll(e));
                    }
                    delete isc.DataSource._doNotClobber;
                    if (callback) this.fireCallback(callback, ["dsID"], [_dsID]);
                },
                {
                    actionURL: url,
                    httpMethod: "GET",
                    willHandleError: true
                }
            );
        } else {
            this.logWarn("DataSource(s) already loaded: " + dsID.join(",") +
                "\nUse forceReload to reload such DataSources");
            if (callback) this.fireCallback(callback, ["dsID"], [_dsID]);
        }
    },

    //> @classMethod dataSource.loadWithParents()
    // Variation of +link{DataSource.load()} that will also automatically load any DataSources that
    // the requested DataSources inherit from (via +link{DataSource.inheritsFrom}).
    // <P>
    // If the parent DataSource is already loaded, calling <code>loadWithParents</code> will not
    // automatically reload them unless the forceReload parameter is passed.
    //
    // @param dsID (String | Array of String) DataSource ID or Array of DataSource IDs
    // @param callback (Function) Callback to fire after DataSource loading completes
    // @param [forceReload] (boolean) Forcibly reload a dataSource if it's already loaded
    //
    // @visibility external
    //<
    loadWithParents : function (dsID, callback, forceReload) {
        this.load(dsID, callback, forceReload, true);
    },

    //> @classMethod DataSource.getSortBy()
    // Given an array of +link{SortSpecifier}s, return a simple list of Strings in the format
    // expected by +link{dsRequest.sortBy}.
    //
    // @param sortSpecifiers (Array of SortSpecifier) The list of specifiers to return in
    // sortBy format
    // @return (Array[] of String) An array of sort-definitions in the format expected by
    // +link{dsRequest.sortBy}
    // @visibility external
    //<
    getSortBy : function (sortSpecifiers) {
        if (!isc.isA.Array(sortSpecifiers)) sortSpecifiers = [sortSpecifiers];

        var sortArray = [];
        for (var i=0; i<sortSpecifiers.length; i++) {
            var item=sortSpecifiers.get(i);
            sortArray.add((!Array.shouldSortAscending(item.direction) ? "-" : "") + item.property);
        }
        return sortArray;
    },

    //> @classMethod DataSource.getSortSpecifiers()
    // Return a an array of +link{SortSpecifier}s, given an array of Strings in the format
    // expected by +link{dsRequest.sortBy}.
    //
    // @param sortBy (Array of String) A list of sortBy strings in the format expected by +link{dsRequest.sortBy}
    // @return (Array of String) An array of +link{SortSpecifier}s equivalent to the passed in string array
    // @visibility external
    //<
    getSortSpecifiers : function (sortBy) {
        if (!isc.isA.Array(sortBy)) sortBy = [sortBy];

        var specifierArray = [];
        for (var i=0; i<sortBy.length; i++) {
            var item = sortBy.get(i),
                direction = "ascending",
                property = item
            ;
            if (item.substring(0,1) == "-") {
                direction = "descending";
                property = item.substring(1);
            }
            specifierArray.add({ property: property, direction: direction})
        }
        return specifierArray;
    },

    // class-level version of isAdvancedCriteria - accepts a DS as a param, passed in by calls
    // from the instance-level method
    isAdvancedCriteria : function (criteria, dataSource) {
        if (!criteria) return false;

        if (!dataSource) {
            // Is it explcitily marked as AdvancedCiteria?
            return (criteria && criteria._constructor == "AdvancedCriteria");
        }

        if (!isc.isA.DataSource(dataSource)) dataSource = this.get(dataSource);

        // we have a dataSource
        if (criteria._constructor == "AdvancedCriteria") return true;

        // Not explicitly marked, so we'll make a guess.
        var returnVal = false,
            searchOp = dataSource.getSearchOperator(criteria.operator)
        ;

        // We'll assume it's AdvancedCriteria if the fieldName property refers to a valid
        // field on this DS, and the operator property refers to a valid operator.  This
        // would be extremely unlikely to occur even if a DataSource existed to store criteria
        // in fieldName/operator/value format, since Records for such a DataSource would not
        // have values for "fieldName" that would equal the fieldNames of such a DataSource (eg
        // "fieldName", "operator" or "value")
        if (dataSource.getField(criteria.fieldName) && searchOp) {
            returnVal = true;
        }

        // Before applying further heuristics, make sure that this DataSource
        // doesn't have any fields that are actually called "fieldName" or "operator"
        else if (dataSource.getField("fieldName") || dataSource.getField("operator")) returnVal = false;

        // if the criteria has fieldName and operator attribues *AND* the operator is
        // valid, assume we have advancedCriteria - this caters for criteria agains calculated
        // fields, which do not appear as DS-fields

        else if (criteria.fieldName && (criteria.operator && searchOp)) {
            returnVal = true;
        }

        // if the criteria has fieldName and value attributes but no operator assume
        // we have advancedCriteria where operator is implied as "equals".
        else if (criteria.fieldName && criteria.value != null && criteria.operator == null) {
            returnVal = true;
        } else {
            // We'll also assume it's an AdvancedCriteria if there is no fieldName property and
            // the operator is either "and" or "or".
            var undef;
            if (criteria.operator != undef) {
                var op = searchOp;
                if (op != null && (op.isAnd || op.isOr)) {
                    returnVal = true;
                }
            }
        }
        this.logInfo("Criteria object:" + this.echo(criteria) + " not explicitly marked as AdvancedCriteria" +
            (returnVal ? " but treating as Advanced based on format." : " - treating as SimpleCriteria."), "AdvancedCriteria");
        return returnVal;
    },

    //> @classMethod DataSource.copyCriteria
    // Create a copy of a criteria.
    //
    // @param criteria (Criteria) criteria to copy
    // @return (Criteria) copy of criteria
    // @visibility external
    //<
    copyCriteria : function (criteria) {
        if (criteria == null) return null;
        return isc.clone(criteria);
    },

    getCriteriaFields : function (criteria, dataSource, isSubcriterion) {
        if (dataSource && !isc.isA.DataSource(dataSource)) dataSource = this.get(dataSource);
        if (isSubcriterion ||
            (dataSource ? dataSource.isAdvancedCriteria(criteria) :
                this.isAdvancedCriteria(criteria))
           )
        {
            var fieldNames = [];
            this._getAdvancedCriteriaFields(criteria, fieldNames);
            return fieldNames;
        }
        return isc.getKeys(criteria);
    },

    // Drill into advancedCriteria object extracting fields
    _getAdvancedCriteriaFields : function (criterion, fieldNames) {
        if (criterion.criteria) {
            for (var i = 0; i < criterion.criteria.length; i++) {
                isc.DS._getAdvancedCriteriaFields(criterion.criteria[i], fieldNames);
            }
        } else {
            fieldNames.add(criterion.fieldName);
        }
    },


    // ---------- LOCAL RESPONSE CACHING ----------
    _responseCache: [],
    maxResponsesToCache: 100,
    cacheResponse : function (dsRequest, dsResponse) {
        if (isc.Offline) {
            var requestKey = isc.Offline.serialize(isc.Offline.trimRequest(dsRequest));
            var index = this._responseCache.findIndex("requestKey", requestKey);
            if (index != -1) {
                this._responseCache.set(index, dsResponse);
            } else {
                if (this._responseCache.length >= this.maxResponsesToCache) {
                    this._responseCache.removeAt(0);
                }
                this._responseCache.add({
                    requestKey: requestKey,
                    dsResponse: dsResponse
                });
            }
        }
    },

    getCachedResponse : function (dsRequest) {
        if (isc.Offline) {
            var requestKey = isc.Offline.serialize(isc.Offline.trimRequest(dsRequest));
            return this._responseCache.find("requestKey", requestKey);
        }
        return null;
    },


    //> @classAttr DataSource.offlineMessage  (string : "This data not available while offline" : IRW)
    // A message returned by a DataSource when it is returning an empty dataset for a fetch
    // because the browser is currently offline and there is no suitable cached offline response.
    // @group i18nMessages, offlineGroup
    // @visibility external
    //<
    offlineMessage: "This data not available while offline",

    //> @classAttr DataSource.maxFileSizeExceededMessage  (string : "Size of '${uploadedFileName}' (${uploadedFileSize}) exceeded maximum allowed file size of ${maxFileSize}" : IRW)
    // A message returned by a DataSource when an uploaded file exceeds dataSourceField.maxFileSize
    // @group i18nMessages
    // @visibility external
    //<
    maxFileSizeExceededMessage: "Size of '${uploadedFileName}' (${uploadedFileSize}) exceeded maximum allowed file size of ${maxFileSize}",


    getPathValue : function (record, fieldPath, field) {
        //var field;
        //if (isc.isAn.Object(fieldPath)) {
        //    field = fieldPath;
        //    fieldPath = null;
        //}
        return isc.Canvas._getFieldValue(fieldPath, field, record, null, true);
    },

    // setPathValue - corollary for getPathValue()
    setPathValue : function (record, fieldPath, field, value) {
        //var field;
        //if (isc.isAn.Object(field)) {
        //    field = fieldPath;
        //    fieldPath = null;
        //}
        isc.Canvas._saveFieldValue(fieldPath, field, value, record, null, true);
    },

    // Given an array of dataSources, return field identifiers for all fields in the dataSources
    // These will be of the form "dataSourceID.fieldName"
    getCombinedDataSourceFields : function (dataSources) {
        var fields = [];
        for (var i = 0; i < dataSources.length; i++) {
            var dataSource = dataSources[i];
            if (isc.isA.String(dataSource)) dataSource = isc.DataSource.get(dataSource);
            if (dataSource == null) {
                this.logWarn("getCombinedDataSourceFields() - unable to locate dataSource:"
                    + dataSources[i]);
                continue;
            }
            var dsID = dataSource.getID(),
                dsFields = isc.getKeys(dataSource.getFields());
            for (var j = 0; j < dsFields.length; j++) {
                fields[fields.length] = dsID + "." + dsFields[j];
            }
        }
        return fields;
    },

    // Given a ds field identifier like "dataSourceId.fieldName", extract the field definition
    // from the appropriate dataSource.
    // The dataSources array is optional - if present, pull from that set of dataSources
    // rather than doing a standard isc.DS.get()


    getFieldFromDataSources : function (combinedFieldId, dataSources, suppressWarning) {
        var dataSource = this.getDataSourceForField(combinedFieldId, dataSources, suppressWarning);
        if (dataSource == null) {
            if (!suppressWarning) {
                this.logWarn("getFieldFromDataSources() - unable to find dataSource " +
                    " from combined field ID " + combinedFieldId);
            }
            return null;
        }

        var splitFieldId = combinedFieldId.split("."),
            dsId = splitFieldId[0],
            dsFieldName = splitFieldId[1];
        return dataSource.getField(dsFieldName);

    },

    getDataSourceForField : function (combinedFieldId, dataSources, suppressWarning) {

        var splitFieldId = combinedFieldId.split("."),
            dsId = splitFieldId[0],
            dsFieldName = splitFieldId[1];
        if (dsId == null || dsFieldName == null) {
            if (!suppressWarning) {
                this.logWarn("getDataSourceForField() passed invalid field ID:" + combinedFieldId +
                    ". Expected format is to use dot-notation to indicate the dataSource plus " +
                    "fieldName - for example 'countryDS.countryCode'");
            }
            return null;
        }
        var dataSource;
        if (dataSources == null) dataSource = isc.DataSource.get(dsId);
        else {
            // sanity check only
            if (!isc.isAn.Array(dataSources)) dataSources = [dataSources];
            for (var i = 0; i < dataSources.length; i++) {
                if (dataSources[i].getID() == dsId) {
                    dataSource = dataSources[i];
                    break;
                }
            }
        }
        return dataSource;
    },

    _getDefaultValueForFieldType : function (fieldType) {
        var value;
        switch (fieldType) {
            case "date":
            case "datetime":
            case "time":
                value = new Date(0);
                break;
            case "float":
                value = 0.0;
                break;
            case "integer":
                value = 0;
                break;
            case "boolean":
                value = false;
                break;
            case "text":
            default:
                value = "";
                break;
        }
        return value;
    },

    _getTypedValueAsString : function (value, fieldType, settings) {
        if (settings == null) settings = {};

        if (value == null) {
            var defaultText = settings.nullValueText;
            if (defaultText != null) return defaultText;
            // if textExportSettings.nullValueText is null, then
            // set value to the default value for the field's type
            value = this._getDefaultValueForFieldType(fieldType);
        }
        switch (fieldType) {
            case "date":
                var format = settings.dateFormat;
                return value.toShortDate(format);
            case "datetime":
                var format = settings.dateTimeFormat;
                return value.toShortDateTime(format);
            case "time":
                var format = settings.timeFormat;
                return isc.Time.toShortTime(value, format);
            case "float":
            case "integer":
            case "boolean":
            default:
                return value.toString();
            case "text":
                return value;
        }
    },

    //> @classMethod DataSource.isFlatCriteria(AdvancedCriteria)
    // Returns true if a given AdvancedCriteria is "flat." That is, the criteria consists of either:
    // <ul>
    // <li> a top-level "and" or "or" +link{Criterion}, where none of the
    //      +link{Criterion.criteria,subcriteria} use +link{LogicalOperator,logical operators},
    //      hence have no subcriteria of their own
    // <li> a single Criterion that is not a logical operator, hence has no subcriteria
    // </ul>
    //
    // @param criteria (AdvancedCriteria) the AdvancedCriteria to check for flatness
    // @return (boolean) true if criteria is flat
    // @visibility external
    //<
    isFlatCriteria : function (criteria) {
        if (!criteria) return false;

        // Is criteria a single Criterion?
        if (isc.isA.emptyObject(criteria) ||
                (criteria.fieldName && criteria.operator && criteria.operator != "and" && criteria.operator != "or"))
        {
            return true;
        }

        // Does criteria have a top-level logical operator but no other
        // logical operators in sub-criteria?
        if (criteria._constructor == "AdvancedCriteria" &&
                (criteria.operator == "and" || criteria.operator == "or") &&
                criteria.criteria)
        {
            var subCriteria = criteria.criteria;
            for (var i = 0; i < subCriteria.length; i++) {
                var criterion = subCriteria[i];
                if (criterion.operator &&
                        (criterion.operator == "and" || criterion.operator == "or" || criterion.operation == "not"))
                {
                    return false;
                }
            }
            return true;
        }
        return false;
    },

    //> @classMethod DataSource.flattenCriteria(AdvancedCriteria)
    // Returns new criteria that has at most one top-level LogicalOperator ("and" or "or").
    // This new criteria will be considered "flat" by +link{isFlatCriteria}.
    // <p>
    // Not all AdvancedCriteria can be flattened and remain logically equivalent.  When
    // criteria will be logically modified by flattening, all criteria that appear anywhere in
    // the AdvancedCriteria structure will appear under a single top-level operator, which
    // will be the same top-level operator as the passed AdvancedCriteria.
    // <p>
    // For example, given criteria like this (in the JSON representation of AdvancedCriteria):
    // <pre>
    //      { operator: "and", criteria: [
    //         { fieldName: "continent", operator: "equals", value: "Europe"},
    //         { operator: "or", criteria: [
    //            { fieldName: "countryName", operator: "iEndsWith", value: "land"},
    //            { fieldName: "population", operator: "lessThan", value: 3000000}
    //         ]}
    //        ]
    //      }
    // </pre>
    // The returned criteria would be:
    // <pre>
    //      { operator: "and", criteria: [
    //         { fieldName: "continent", operator: "equals", value: "Europe"},
    //         { fieldName: "countryName", operator: "iEndsWith", value: "land"},
    //         { fieldName: "population", operator: "lessThan", value: 3000000}
    //       ]}
    // </pre>
    // This returned criteria is not logically equivalent to the passed criteria - the
    // "iEndsWith" and "lessThan" criteria that were formerly nested under a logical "or"
    // operator must now <i>both</i> be satisfied instead of <i>either</i> being satisfied.
    // You can use +link{canFlattenCriteria()} to detect whether an AdvancedCriteria is going
    // to be changed by <code>flattenCriteria()</code>.
    // <p>
    // Because the returned criteria may not be logically equivalent,
    // <code>flattenCriteria</code> should not be used as a means of simplifying criteria to
    // make server implementation easier or anything of the kind.  The primary purpose of
    // returning logically different criteria is to enable an end user to switch from an
    // interface for editing nested criteria to an interface that can't handle nested
    // criteria and convert the criteria while preserving as much as possible.
    //
    // @param criteria (AdvancedCriteria) the AdvancedCriteria to flatten
    // @return (AdvancedCriteria) flattened criteria
    // @visibility external
    //<
    flattenCriteria : function (criteria) {
        if (!criteria) return null;

        // Start with top-level criteria
        var flatCriteria = isc.addProperties({}, criteria);

        // Add all sub-criteria as a single criteria list
        if (flatCriteria.criteria) {
            flatCriteria.criteria = [];
            this._addSubCriteriaToList(flatCriteria.criteria, criteria);
        }

        return flatCriteria;
    },
    _addSubCriteriaToList : function (list, criteria) {
        if (!criteria.criteria) {
            list.add(isc.addProperties({}, criteria));
            return;
        }
        var subCriteria = criteria.criteria;
        for (var i = 0; i < subCriteria.length; i++) {
            var criterion = subCriteria[i];
            this._addSubCriteriaToList(list, criterion);
        }
    },

    //> @classMethod DataSource.canFlattenCriteria(AdvancedCriteria)
    // Returns true if calling +link{flattenCriteria()} on the passed criteria would produce
    // logically equivalent criteria.
    //
    // @param criteria (AdvancedCriteria) the AdvancedCriteria to check for flatness
    // @return (boolean) true if criteria can be flattened without logical change
    // @visibility external
    //<
    canFlattenCriteria : function (criteria) {
        if (!criteria) return false;

        // If criteria is already flat, it can be flattened.
        if (isc.DataSource.isFlatCriteria(criteria)) return true;

        // If top-level operator and all sub-criteria operators
        // are the same, the criteria can be flattened.
        var topLevelOperator = criteria.operator;
        if (this._criteriaHasSameLogicalOperators(criteria, topLevelOperator)) return true;

        return false;
    },
    _criteriaHasSameLogicalOperators : function (criteria, operator) {
        if (!criteria.criteria) return true;
        if (criteria.operator != operator) return false;

        var subCriteria = criteria.criteria;
        for (var i = 0; i < subCriteria.length; i++) {
            var criterion = subCriteria[i];
            if (!this._criteriaHasSameLogicalOperators(criterion, operator)) return false;
        }

        return true;
    },

    //> @classMethod DataSource.getAdvancedCriteriaDescription()
    // Returns a human-readable string describing the clauses in this advanced criteria or criterion.
    //
    // @param criteria (AdvancedCriteria or Criterion) Criteria to convert to a readable string
    // @param dataSource (DataSource) DataSource to provide definitions of operators
    // @return (String) Human-readable string describing the clauses in the passed criteria
    // @visibility external
    //<
    getAdvancedCriteriaDescription : function (criteria, dataSource) {

        // support being passed an array of dataSources, and using dot-notation on the fieldNames
        // to extract field info
        if (!isc.isAn.Array(dataSource)) {
            if (!isc.isA.DataSource(dataSource)) dataSource = isc.DS.getDataSource(dataSource);
            if (!dataSource) return "No dataSource";
        }

        var result = "";

        if (criteria.criteria && isc.isAn.Array(criteria.criteria)) {
            // complex criteria, call this method again with that criteria
            var operator = criteria.operator,
            subCriteria = criteria.criteria;

            for (var i = 0; i<subCriteria.length; i++) {
                var subItem = subCriteria[i];

                if (i > 0) result += " " + operator + " ";
                if (subItem.criteria && isc.isAn.Array(subItem.criteria)) {
                    result += "("
                        result += isc.DataSource.getAdvancedCriteriaDescription(subItem, dataSource);
                    result += ")"
                } else {
                    result += isc.DataSource.getCriterionDescription(subItem, dataSource);
                }
            }
        } else {
            // simple criterion
            result += isc.DataSource.getCriterionDescription(criteria, dataSource);
        }

        return result;
    },

    // helper method to return the description of a single criterion
    getCriterionDescription : function (criterion, dataSource) {
        if (criterion == null) return "";
        var fieldName = criterion.fieldName,
        operatorName = criterion.operator,
        start = criterion.start,
        end = criterion.end,
        field;
        if (isc.isAn.Array(dataSource)) {
            dataSource = isc.DataSource.getDataSourceForField(fieldName, dataSource);
            // At this point we've identified the dataSource, but the fieldName is dot-prefixed, so
            // use getFieldFromDataSources to get the field object.
            field = isc.DataSource.getFieldFromDataSources(fieldName, [dataSource]);
        } else {
            if (!isc.isA.DataSource(dataSource)) dataSource = isc.DS.getDataSource(dataSource);
            if (!dataSource) return "No DataSource";
            field = dataSource.getField(fieldName);
            if (field == null) {
                field = dataSource.getFieldForDataPath(fieldName);
            }
        }
        var operator = dataSource.getSearchOperator(operatorName, field),
        operatorMap = dataSource.getFieldOperatorMap(field, true, operator.valueType, false),
        result=""
            ;

        if (!field) {
            if (criterion.criteria && isc.isAn.Array(criterion.criteria)) {
                // we've been passed an AdvancedCriteria as a simple criterion - log a warning and
                // return the result of getAdvancedCriteriaDescription(), rather than just bailing
                isc.logWarn("DataSource.getCriterionDescription: Passed an AdvancedCriteria - "+
                "returning through getAdvancedCriteriaDescription.");
                return isc.DataSource.getAdvancedCriteriaDescription(criterion, dataSource);
            }

            // just an unknown field - log a warning and bail

            result = fieldName + " " + isc.FilterBuilder.missingFieldPrompt + " ";

            isc.logWarn("DataSource.getCriterionDescription: No such field '"+fieldName+"' "+
                    "in DataSource '"+dataSource.ID+"'.");
        } else {
            result = (field.title ? field.title : fieldName) + " ";
        }
        switch (operatorName)
        {
        case "lessThan":
        case "greaterThan":
        case "lessOrEqual":
        case "greaterOrEqual":
        case "between":
        case "notNull" :
            result += "is " + (operatorMap[operatorName] || operatorName);
            break;
        case "equals":
            result += "is equal to";
            break;
        case "notEqual":
            result += "is not equal to";
            break;
        default: result += operatorMap[operatorName] || operatorName;
        }
        var value = criterion.value;
        if (isc.DateUtil.isRelativeDate(value)) {
            // if passed a RelativeDate, convert it to an absolute one for display
            var type = field ? field.type : null,
                    isLogicalDate = false,
                    sType = isc.SimpleType
                    ;
            if (sType.inheritsFrom(type, "date") && !sType.inheritsFrom(type, "datetime")) {
                isLogicalDate = true;
            }

            value = isc.DateUtil.getAbsoluteDate(value, null, null, isLogicalDate);
        }

        if (operator.valueType == "valueRange") result += " " + start + " and " + end;
        else if (operatorName != "notNull") {
            result += " ";
            if (isc.isA.Date(value)) {
                // handle dates separately
                if (value.isLogicalDate) result += value.toShortDate();
                else result += value.toShortDatetime();
            } else result += value && value.toString ? value.toString() : value;
        }

        return result;
    },

    //> @classMethod DataSource.getLoaderURL()
    // Returns the +link{}DataSource.setLoaderURL(),loaderURL}
    //
    // @return (String) The loaderURL
    // @visibility external
    //<
    getLoaderURL : function() {
        return isc.DataSource.loaderURL;
    },

    //> @classMethod DataSource.setLoaderURL()
    // Sets the URL where the DataSourceLoader servlet has been installed; this is used by the
    // +link{DataSource.load()} method.  Note, one reason you may wish to modify the loader URL
    // is to include a Cross-Site Request Forgery (CSRF) token, as described
    // +link{RPCManager.actionURL,here}
    //
    // @param url (String) The new loaderURL
    // @visibility external
    //<
    setLoaderURL : function(url) {
        isc.DataSource.addClassProperties({ loaderURL: url });
    }
    


});


isc.DataSource.addProperties({
    //> @attr dataSource.ID              (string : null : [IR])
    // Unique identifier for this DataSource.  Required for all DataSources.
    //
    // <smartclient>DataSources will make themselves available as JavaScript globals under
    // the same name as their ID only if +link{addGlobalId} is set.</smartclient>
    //
    // @group identity
    // @serverDS allowed
    // @see group:memoryLeaks
    // @visibility external
    //<

    //> @attr dataSource.addGlobalId    (boolean : true : IRA)
    // Whether to make this DataSource available as a global variable for convenience.
    // @group identity
    // @visibility external
    //<
    addGlobalId:true,

    //> @attr dataSource.dataFormat (DSDataFormat : "iscServer" : [IR])
    // Indicates the format to be used for HTTP requests and responses when
    // fulfilling DSRequests (eg, when +link{DataSource.fetchData()} is called).
    //
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    // @example jsonDataSource
    // @example simpleJSON
    //<
    dataFormat:"iscServer",

    //> @attr dataSource.useStrictJSON (boolean : null : IR)
    // Should HTTP responses to requests by this dataSource be formatted using the
    // strict JSON subset of the javascript language?
    // If set to true, responses returned by the server should match the format described
    // +externalLink{http://www.json.org/js.html,here}.
    // <P>
    // Only applies to dataSources which send requests to a server and have
    // +link{dataSource.dataFormat} set to "json" or "iscServer".
    // <P>
    // <b>Note:</b> using strict JSON avoids a known issue in Internet Explorer 9 where
    // datasource transactions can leak memory due to a browser behavior where the native
    // <code>eval()</code> method fails to clean up references when the objects
    // go out of scope. See +link{isc.RPCManager.allowIE9Leak} for more on this.
    // @visibility external
    //<

    //> @attr dsRequest.useStrictJSON (boolean : null : IR)
    // Should the HTTP response to this request be formatted using the
    // strict JSON subset of the javascript language?
    // If set to true, responses returned by the server should match the format described
    // +externalLink{http://www.json.org/js.html,here}.
    // <P>
    // Only applies to requests sent a server with
    // +link{dataSource.dataFormat} set to "json" or "iscServer".
    // @visibility external
    //<



    //> @attr dataSource.dataProtocol (DSProtocol : null : [IR])
    // Controls the format in which inputs are sent to the dataURL when fulfilling DSRequests.
    // May be overridden for individual request types using
    // +link{OperationBinding.dataProtocol,operation bindings}.
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.useHttpProxy (boolean : null : IR)
    // Like +link{operationBinding.useHttpProxy}, but serves as a default for this DataSource
    // that may be overridden by individual operationBindings.
    //
    // @group clientDataIntegration
    // @visibility external
    //<

    //> @attr dataSource.callbackParam (String : "callback" : [IR])
    // Applies only to dataFormat: "json" and +link{dataTransport}:"scriptInclude".  Specifies
    // the name of the query parameter that tells your JSON service what function to call as
    // part of the response.
    //
    // @see dataSource.dataFormat
    // @see dataSource.operationBindings
    // @see operationBinding.callbackParam
    // @group clientDataIntegration
    // @visibility external
    // @serverDS allowed
    // @example xmlEditSave
    //<
    callbackParam : "callback",

    //> @attr dataSource.requestProperties (DSRequest Properties : null : [IR])
    // Additional properties to pass through to the +link{DSRequest}s made by this
    // DataSource.
    // <p>
    // These properties are applied before +link{dataSource.transformRequest} is called.
    //
    // @see DSRequest
    // @see operationBinding.requestProperties
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.dataTransport      (RPCTransport : RPCManager.defaultTransport : [IR])
    // Transport to use for all operations on this DataSource. Defaults to
    // +link{RPCManager.defaultTransport}.  This would typically only be set to
    // enable "scriptInclude" transport for contacting +link{DataSource.dataFormat,JSON} web
    // services hosted on servers other than the origin server.
    // <p>
    // When using the "scriptInclude" transport, be sure to set +link{dataSource.callbackParam} or
    // +link{operationBinding.callbackParam} to match the name of the query parameter name expected
    // by your JSON service provider.
    //
    // @see RPCTransport
    // @see dataSource.callbackParam
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    //<

    //> @attr dataSource.preventHTTPCaching (Boolean : true : IR)
    // If set, the DataSource will ensure that it never uses a cached HTTP response, even if the
    // server marks the response as cacheable.
    // <P>
    // Note that this does not disable caching at higher levels in the framework, for example, the
    // caching performed by +link{ResultSet}.
    // @serverDS allowed
    // @visibility external
    //<
    preventHTTPCaching:true,

    //> @type DSDataFormat
    // Indicates the format to be used for HTTP requests and responses when
    // fulfilling DSRequests (eg, when +link{DataSource.fetchData()} is called).
    // <P>
    // Note that +link{RPCManager.startQueue(),request queuing} is only available for
    // "iscServer" requests.
    //
    // @value "iscServer"
    // Make an HTTP request in a format recognized by the ISC server and expect ISC server
    // response format.  +link{group:serverDataIntegration,Server-side integration} can then be
    // used to connect to any kind of data store or web service.
    //
    // @value "xml"
    // Expect XML responses.  Request format depends on the setting for
    // +link{operationBinding.dataProtocol,protocol}.  This is the correct setting when
    // consuming RSS feeds, XML web services (whether SOAP, REST, XML-RPC or custom format),
    // and XML flat files directly from the browser.
    // <P>
    // Values for "date", "time" or "datetime" fields in responses should be specified in the
    // applicable <a target=_blank href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML Schema
    // date format</a>.
    // If no timezone is explicitly specified, dates / datetimes received by the client are
    // assumed to be GMT. Note that "date" type fields represent logical dates and may omit
    // time information entirely, and "time" type fields may omit date information.
    // See +link{group:dateFormatAndStorage,Date and Time Format and storage} for more information
    // on how date values are serialized in requests sent to the server.
    //
    // <P>
    // A DSResponse will be derived from the returned XML via the process described under
    // +link{dataSource.transformResponse()}.
    //
    // @value "json"
    // Expect response in JSON +externalLink{http://json.org,(Java Script Object Notation)}
    // format, ready to be eval()'d. Response should either be a naked object literal:
    // <pre>
    //     { "property":"value1", "property2" : "value2", ... }
    // </pre>
    // or a string that evals to return a valid response object:
    // <pre>
    //     var response = { "property":"value1", "property2" : "value2", ... }
    //     response;
    // </pre>
    // <P>
    // A DSResponse will be derived from the returned JSON via the process described under
    // +link{dataSource.transformResponse()}.
    // <P>
    // As with <code>"xml"</code> responses, values for "date" or "datetime" fields should be
    // specified as a String in
    // <a target=_blank href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML Schema date format</a>
    // and may include a timezone.  In the absence of a timezone they will be assumed to be GMT.
    // <P>
    // Request format depends on the setting for +link{operationBinding.dataProtocol,protocol}.
    // See also +link{XJSONDataSource}.
    //
    // @value "custom"
    // SmartClient will not attempt to parse the response, instead,
    // +link{dataSource.transformResponse()} must be implemented.
    // <code>transformResponse</code> will receive the "data" parameter as a String, and must
    // parse this String into an Array of Objects, which should be set as
    // +link{dsResponse.data}.  Request format depends on the setting for
    // +link{operationBinding.dataProtocol,protocol}.
    // <P>
    // Note that, unlike either the "json" or "xml" settings of <code>dataFormat</code>, you
    // are responsible for ensuring that parsed values are the correct type, for example, using
    // the JavaScript built-ins <code>parseInt</code> and <code>parseFloat</code> on integer
    // and decimal values respectively, and using <code>new Date()</code> to construct valid
    // Dates.
    //
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @visibility external
    // @serverDS allowed
    // @example simpleJSON
    //<

    //> @attr dataSource.serverType (DSServerType : "generic" : [IR])
    // For a DataSource stored in .xml format on the SmartClient server, indicates what server-side
    // connector to use to execute requests, that is, what happens if you call
    // dsRequest.execute() in server code.
    //
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<

    //> @type DSServerType
    // Indicates what SmartClient Server will do with a DataSource request if you call
    // dsRequest.execute() in server code.
    // <P>
    // If you use a Java-based persistence layer not provided by SmartClient, such as
    // EJB or your own custom object model, you don't need to set
    // <code>dataSource.serverType</code> and should follow the
    // +link{group:serverDataIntegration,integration instructions}.
    //
    // @value "sql"
    // Use SmartClient's +link{group:sqlDataSource,built-in SQL connectors} to talk directly
    // to relational databases.
    //
    // @value "hibernate"
    // Use Hibernate, either using a real mapped bean or by automatically generating a
    // Hibernate configuration based on a SmartClient DataSource file
    // (<i>dataSourceID</i>.ds.xml).  See +link{group:hibernateIntegration} for details.
    //
    // @value "jpa"
    // Use SmartClient's built-in JPA 2.0 connector.
    //
    // @value "jpa1"
    // Use SmartClient's built-in JPA 1.0 connector.
    //
    // @value "generic"
    // Requests will be delivered to the server and you are expected to write Java code to
    // create a valid response.  Throws an error if the server side method dsRequest.execute()
    // is called. This is appropriate if you intend an entirely custom implementation, and you
    // want an error thrown if there is an attempt to call an operation you have not
    // implemented.
    //
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.tableName (String : null : [IR])
    // For DataSources using the +link{group:sqlDataSource,SmartClient SQL engine} for
    // persistence, what database table name to use.  The default is to use the DataSource ID
    // as the table name.
    //
    // @see dataSource.quoteTableName
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.schema (String : null : [IR])
    // <b>This property only applies to the built-in SQL DataSource provided in Pro and better
    // editions of SmartClient</b>
    // <p>
    // Defines the name of the schema we use to qualify the +link{tableName,tableName} in generated
    // SQL.  If you do not provide this property, table names will not be qualified in generated
    // SQL, and thus the default schema will be used.  Support for multiple schemas (or schemata)
    // varies quite significantly across the supported databases, as does the meaning of the
    // phrase "default schema".  In addition, some databases allow you to override the default
    // schema in the JDBC connection URL, which is a preferable approach if all your tables are
    // in the same (non-default) schema.
    // <p>
    // The following table provides information by product:
    // <p>
    // <table border="1" class="normal" width="90%">
    // <tr><td> Product</td><td width="90%"> Notes</td></tr>
    // <tr><td> DB2</td><td>
    // Arbitrarily named schemas are supported.  The default schema is named after the connecting
    // user, though this can be overridden by specifying the "currentSchema" property on the JDBC
    // connection URL</td></tr>
    // <tr><td> DB2 for iSeries</td><td>
    // Arbitrarily named schemas are supported.  "Schema" is synonymous with "library".  The
    // default schema depends on the setting of the "naming" connection property.  When this is
    // set to "sql", behavior is similar to other DB2 editions: the default schema is named after
    // the connecting user, unless overridden by specifying a library name in the JDBC connection
    // URL.  When "naming" is set to "system", the schema of an unqualified table is resolved
    // using a traditional search of the library list; the library list can be provided in the
    // "libraries" property</td></tr>
    // <tr><td> Firebird</td><td>
    // Firebird does not support the concept of schema at all - all "schema objects" like tables
    // and indexes belong directly to the database.  In addition, Firebird actively rejects
    // qualified table names in queries as syntax errors; therefore, you should <b>not</b> set
    // the <code>schema</code> property for a DataSource that will be backed by a Firebird database
    // </td></tr>
    // <tr><td> HSQLDB</td><td>
    // Arbitrarily named schemas are supported.  The default schema is auto-created when the
    // database is created; by default it is called "PUBLIC", but can be renamed.  It is not
    // possible to set the default schema in the JDBC connection URL</td></tr>
    // <tr><td> Informix</td><td>
    // Informix databases can be flagged as "ANSI mode" at creation time.  ANSI-mode databases
    // behave similarly to DB2 for schema support: arbitrarily named schemas are supported, and
    // the default schema is the one named after the connected user.  Non-ANSI databases have no
    // real schema support at all. It is not possible to set the default schema in the JDBC
    // connection URL with either type of database</td></tr>
    // <tr><td> Microsoft SQL Server</td><td>
    // Prior to SQL Server 2005, schema support is similar to Oracle: "schema" is synonymous with
    // "owner".  As of SQL Server 2005, schema is supported as a separate concept, and a user's
    // default schema can be configured (though it still defaults to a schema with the same name
    // as the user).  It is not possible to set the default schema in the JDBC connection URL</td></tr>
    // <tr><td> MySQL</td><td>
    // MySQL does not have a separate concept of "schema"; it treats the terms "schema" and
    // "database" interchangeably.  In fact MySQL databases actually behave more like schemas, in
    // that a connection to database X can refer to a table in database Y simply by qualifying
    // the name in the query.  Also, because schema and database are the same concept in MySQL,
    // overriding the "default schema" is done implicitly when you specify which database to
    // connect to in your JDBC connection URL</td></tr>
    // <tr><td> Oracle</td><td>
    // Arbitrarily named schemas are not supported; in Oracle, "schema" is synonymous with "user",
    // so each valid user in the database is associated implicitly with a schema of the same name,
    // and there are no other schemas possible.  It is possible to refer to tables in another
    // user's schema (assuming you have the privileges to do so) by simply qualifying the table name.
    // The default schema is always implied by the connecting user and cannot be overridden.</td></tr>
    // <tr><td> Postgres</td><td>
    // Arbitrarily named schemas are supported.
    // Rather than the concept of a "default schema", Postgres supports the idea of a search path
    // of schemas, whereby unqualified table references cause a search of the list of schemas in
    // order, and the first schema in the path is the "current" one for creation purposes.
    // Unfortunately, there is no way to specify this search path on the JDBC connection URL, so
    // the default schema comes from the user definition, ultimately defaulting to the default
    // "public" schema</td></tr>
    // </table>
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.quoteTableName (Boolean : null : [IRA])
    // For SQL DataSources, tells the framework whether to surround the associated
    // +link{dataSource.tableName,table name} with quotation marks whenever it appears in
    // generated queries.  This is only required if you have to connect to a table with a name
    // that is in breach of your database product's naming conventions.  For example, some
    // products (eg, Oracle) internally convert all unquoted references to upper case, so if
    // you create a table called <code><b>myTest</b></code>, the database actually calls it
    // <code><b>MYTEST</b></code> unless you quoted the name in the create command, like this:
    // <p>
    // <code><b>&nbsp;&nbsp;CREATE TABLE "myTest"</b></code>
    // <p>
    // If you <em>do</em> quote the name like this, or if you have to connect to a legacy table
    // that has been named in this way, then you must set this property to tell the SQL engine
    // that it must quote any references to this table name (this requirement depends on the
    // database in use - as noted below, some are not affected by this problem).  If you do
    // not, you will see exceptions along the lines of "Table or view 'myTest' does not exist".
    // <p>
    // Note, other database products (eg, Postgres) convert unquoted names to lower case, which
    // leads to the same issues.  Still others (eg, SQL Server) are not case sensitive and are
    // not affected by this issue.
    // <p>
    // Generally, we recommend that you avoid using this property unless you have a specific
    // reason to do so.  It is preferable to avoid the issue altogether by simply not quoting
    // table names at creation time, if you are able to do so.
    //
    // @see dataSource.tableName
    // @see dataSource.quoteColumnNames
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<


    //> @attr dataSource.dbName (String : null : [IR])
    // For DataSources using the +link{group:sqlDataSource,SmartClient SQL engine} for
    // persistence, which database configuration to use.  Database configurations can be
    // created using the +link{group:adminConsole, Admin Console}.  If unset, the default
    // database configuration is used (which is also settable using the "Databases" tab).
    //
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.configBean (String : null : [IRA])
    // For DataSources of +link{serverType,serverType} "hibernate", the name of a Spring
    // bean to query to obtain Hibernate Configuration for this particular DataSource.  Note
    // that this is intended for DataSource-specific configuration overrides for unusual
    // circumstances, such as a DataSource whose physical data store is a completely
    // different database to that used by other DataSources.  See the
    // +link{group:hibernateIntegration,Integration with Hibernate} article for more
    // information
    //
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.quoteColumnNames (boolean : null : [IR])
    // If set, tells the SQL engine to quote column names in all generated DML and DDL
    // statements for this dataSource.  This will ensure that queries generated against
    // tables that do not follow the database product's natural column-naming conventions
    // will still work.
    // <p>
    // In general we recommend that you allow the database to use its natural naming scheme
    // when creating tables (put more simply, just do not quote column names in the
    // <code>CREATE TABLE</code> statement); if you do this, you will not need to worry about
    // quoting column names when querying.  However, if you are dealing with pre-existing
    // tables, or do not have control over the database naming conventions used, this property
    // may become necessary.  This property may also be necessary if you are using field/column
    // names that clash with reserved words in the underlying database (these vary by database,
    // but a field called "date" or "timestamp" would have problems with most database products)
    // <p>
    // <b>Note:</b> Only applicable to dataSources of +link{attr:serverType,serverType} "sql".
    //
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.jsonPrefix (String : null : [IRA])
    // Allows you to specify an arbitrary prefix string to apply to all json format responses
    // sent from the server to this application.
    // <P>
    // The inclusion of such a prefix ensures your code is not directly executable outside of
    // your application, as a preventative measure against
    // +externalLink{http://www.google.com/search?q=javascript+hijacking, javascript hijacking}.
    // <P>
    // Only applies to responses formatted as json objects. Does not apply to responses returned
    // via scriptInclude type transport.<br>
    // Note: If the prefix / suffix served by your backend is not a constant, you can use
    // +link{operationBinding.dataFormat, dataFormat:"custom"} instead and explicitly parse
    // the prefix out as part of +link{dataSource.transformResponse(), transformResponse()}.
    //
    // @see operationBinding.dataFormat
    // @see operationBinding.dataTransport
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.jsonSuffix (String : null : [IRA])
    // Allows you to specify an arbitrary suffix string to apply to all json format responses
    // sent from the server to this application.
    // <P>
    // The inclusion of such a suffix ensures your code is not directly executable outside of
    // your application, as a preventative measure against
    // +externalLink{http://www.google.com/search?q=javascript+hijacking, javascript hijacking}.
    // <P>
    // Only applies to responses formatted as json objects. Does not apply to responses returned
    // via scriptInclude type transport.
    //
    // @see operationBinding.dataFormat
    // @see operationBinding.dataTransport
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.fields         (Array of DataSourceField : null : [IR])
    // The list of fields that compose records from this DataSource.
    // <P>
    // Each DataSource field can have type, user-visible title, validators, and other metadata
    // attached.
    //
    // @group fields
    // @see class:DataSourceField
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.inheritsFrom (DataSource ID : null : IR)
    // ID of another DataSource this DataSource inherits its +link{fields} from.
    // <P>
    // Local fields (fields defined in this DataSource) are added to inherited fields
    // to form the full set of fields.  Fields with the same name are merged in the same way
    // that +link{DataBoundComponent.fields,databound component fields} are merged with
    // DataSource fields.
    // <P>
    // The default order of the combined fields is new local fields first (including any fields
    // present in the parent DataSource which the local DataSource re-declares), then parent
    // fields.  You can set +link{useParentFieldOrder} to instead use the parent's field
    // order, with new local fields appearing last.  You can set +link{showLocalFieldsOnly} to
    // have all non-local fields hidden.
    // <P>
    // Note that <b>only fields are inherited</b> - other properties such as dataURL and
    // dataFormat are not.  You can use ordinary inheritance, that is, creating a subclass of
    // DataSource, in order to share properties such as dataURL across a series of DataSources
    // that also inherit fields from each other via <code>inheritsFrom</code>.
    // <P>
    // This feature can be used for:
    // <ul>
    // <li>creating a customized view (eg, only certain fields shown) which will be used by
    // multiple +link{DataBoundComponent,databound components}.
    // <li>adding presentation-specific attributes to metadata that has been automatically
    // derived from +link{XMLTools.loadXMLSchema,XML Schema} or other metadata formats
    // <li>modeling object subclassing and extension in server-side code and storage systems
    // <li>modeling relational database joins, and the equivalents in other systems
    // <li>creating hooks for others to customize your application in a maintainable way.  For
    // example, if you have a dataSource "employee", you can create a dataSource
    // "customizedEmployee" which inherits from "employee" but does not initially define any
    // fields, and bind all +link{DataBoundComponent,databound components} to
    // "customizedEmployee".  Customizations of fields (including appearance changes, field
    // order, new fields, hiding of fields, and custom validation rules) can be added to
    // "customizedEmployee", so that they are kept separately from the original field data and
    // have the best possible chance of working with future versions of the "employee"
    // dataSource.
    // </ul>
    //
    // @group fields
    // @serverDS allowed
    // @visibility external
    // @example schemaChaining
    //<


    //> @type DSInheritanceMode
    // For DataSources of type "sql" and "hibernate", specifies the kind of inheritance to use when a dataSource
    // specifies +link{dataSource.inheritsFrom,inheritsFrom}.
    //
    // @value "full"
    // Inherit fields by copying them onto the inheriting DataSource's underlying table.  When we
    // import a DataSource with this inheritanceMode, we create actual columns for inherited fields
    //  on the table we create.  With this inheritanceMode, inherited fields are updatable.
    //
    // @value "none"
    // Do not physically inherit fields onto the inheriting DataSource's SQL table.  Columns will
    // not be created for inherited fields on import, and all generated SQL operations will exclude
    // inherited fields.  However, those fields are still part of the DataSource's schema so you
    // can, for example, write +link{group:customQuerying,custom SQL} that returns values for
    // the inherited fields, and they will be delivered to the client.
    //
    // @group fields
    // @serverDS only
    // @visibility external
    //<




    //> @attr dataSource.inheritanceMode (DSInheritanceMode : "full" : IR)
    // For dataSources of +link{serverType,serverType} "sql" and "hibrenate", specifies the inheritance
    // mode to use.  This property has no effect for any other type of DataSource.
    //
    // @see dataSource.inheritsFrom
    // @group fields
    // @serverDS only
    // @visibility external
    //<


    //> @attr dataSource.sequenceMode         (SequenceMode : "native" : [IR])
    // For fields of +link{dataSourceField.type,type} "sequence" in a dataSource of
    // +link{DataSource.serverType,serverType} "sql", indicates the
    // +link{type:SequenceMode} to use.  This property has no effect for fields or dataSources of
    // other types.
    // <p>
    // You can set a default sequenceMode for all DataSources of a given database type by setting
    // property "sql.{database_type}.default.sequence.mode" in <code>server.properties</code>.
    // You set a global default sequenceMode that applies to all database types by setting property
    // "sql.default.sequence.mode".  For example:<pre>
    //   sql.mysql.default.sequence.mode: jdbcDriver
    // </pre>
    // <P>
    //
    // @serverDS only
    // @visibility external
    //<

    //> @type SequenceMode
    // The possible types of sequence handling SmartClient Server can apply.  This refers to the
    // technique used to obtain the primary keys of the most recent insert, which the product
    // uses to enable automatic cache synchronization (updating client-side components bound to a
    // dataSource to reflect updates to that dataSource).  Only applicable to
    // +link{class:DataSourceField,fields} of +link{type:FieldType,type} "sequence".
    // <P>
    // @value "jdbcDriver" Use the JDBC 3.0 API "getGeneratedKeys()" to get the most recent
    //                     sequence value.  Obviously, this is only an option for JDBC 3.0+ drivers
    // @value "native"     Use a database-specific native technique to obtain the most recent
    //                     sequence value.  The actual technique used varies widely depending on
    //                     the vagaries of the underlying database (and sometimes the vagaries of
    //                     particular releases of a database product)
    // @value "none"       No automatic attempt is made to retrieve the most recent sequence value.
    //                     You are expected to handle this by providing a
    //                     +link{operationBinding.cacheSyncOperation,cacheSyncOperation} that is
    //                     able to return the entire row without needing generated PK values for
    //                     context. For example, a query that uses <code>MAX(pk)</code> would be
    //                     capable of this.  To give a more complex example, say you have a
    //                     sequence value that is retrieved from a legacy system: you could store
    //                     that sequence value in the HTTP session and then have your custom
    //                     <code>cacheSyncOperation</code> reference that session attribute in
    //                     its <code>WHERE</code> clause.  Also note that cacheSyncOperations,
    //                     like any other +link{class:OperationBinding,DataSource operation},
    //                     can be +link{operationBinding.serverObject,written in Java} or any
    //                     +link{operationBinding.script,JSR223-compliant scripting language} -
    //                     you do not have to use SQL
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.useFlatFields (Boolean : null : IR)
    // Like +link{dataBoundComponent.useFlatFields}, but applies to all DataBound components
    // that bind to this DataSource.
    //
    // @group fields
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.showLocalFieldsOnly (boolean : null : IR)
    // For a DataSource that inherits +link{fields} from another DataSource
    // (via +link{inheritsFrom}), indicates that only the fields listed in this DataSource
    // should be shown.  All other inherited parent fields will be marked "hidden:true".
    //
    // @group fields
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.useParentFieldOrder (boolean : null : IR)
    // For a DataSource that inherits +link{fields} from another DataSource
    // (via +link{inheritsFrom}), indicates that the parent's field order should be used
    // instead of the order of the fields as declared in this DataSource.  New fields, if any,
    // are placed at the end.
    //
    // @group fields
    // @serverDS allowed
    // @visibility external
    // @example schemaChaining
    //<

    //> @attr dataSource.dropExtraFields     (boolean : null : IR)
    // Indicates that for server responses, for any data being interpreted as DataSource records,
    // only data that
    // corresponds to declared fields should be retained; any extra fields should be discarded.
    // <P>
    // For +link{DataSource.dataFormat,JSON} data, this means extra properties in selected
    // objects are dropped.
    // <P>
    // By default, for DMI DSResponses, DSResponse.data is filtered on the server to just
    // the set of fields defined on the DataSource.  This type of filtering can also be enabled
    // for non-DMI DSResponses (see the overview in +link{group:dmiOverview,DMI}).  Setting this property to
    // <code>false</code> disables this filtering for this DataSource only.  This setting
    // overrides the configuration in +link{group:server_properties,server.properties}.  This setting can
    // be overridden by +link{ServerObject.dropExtraFields}.
    //
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.sendExtraFields     (Boolean : true : IR)
    // Analogous to +link{dataSource.dropExtraFields}, for data sent to the server.  Setting this
    // attribute to false ensures that for any records in the data object, only fields that
    // correspond to declared dataSource fields will be present on the dsRequest data object passed
    // to +link{dataSource.transformRequest()} and ultimately sent to the server.
    //
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility external
    //<
    sendExtraFields:true,

    //> @attr dataSource.autoDeriveSchema (boolean : null : IR)
    // This property allows you to specify that your DataSource's schema (field definitions) should
    // be automatically derived from some kind of metadata.  This causes SmartClient to create
    // a special super-DataSource, which is used purely as a source of default schema for this
    // DataSource; this is arranged by causing the autoDerived DataSource to automatically
    // +link{inheritsFrom,inherit from} the special super-DataSource.  This allows you to
    // auto-derive schema from existing metadata, whilst still being able to override any or all
    // of it as required.<p>
    // This property has a different implementation depending upon the
    // +link{dataSource.serverType,serverType} of the DataSource:<ul>
    // <li>For a DataSource with serverType: "sql", automatically derive the dataSource's schema
    // from the Spring bean or Java class specified in +link{schemaBean,schemaBean}.  If
    // <code>schemaBean</code> is not specified, derive the schema from the columns in the SQL
    // table specified in +link{tableName,tableName}.  More information on SQL DataSources
    // is +link{group:sqlDataSource,here}</li>
    // <li>For a DataSource with serverType: "hibernate", automatically derive the dataSource's
    // schema from the Spring bean, Hibernate mapping or Java class named in the
    // +link{schemaBean,schemaBean} property.  If no such thing exists, derive the schema from
    // the Hibernate mapping or Java class specified in the +link{beanClassName,beanClassName}
    // property (this allows you to specify schema and mapping separately, in the unusual
    // circumstance that you have a need to do so).  Note that the "mappings" referred to here
    // can mean either <code>.hbm.xml</code> files or annotated classes; both are supported.
    // If neither of these is successful, derive the schema from the underlying SQL table
    // specified in +link{tableName,tableName}.  More information on Hibernate DataSources is
    // +link{group:hibernateIntegration,here}</li>
    // <li>For a DataSource with serverType: "jpa1" or "jpa", automatically derive the
    // dataSource's schema from the annotated JPA class named in the +link{schemaBean,schemaBean}
    // property.  If the schemaBean property is not defined, derive the schema from the
    // annotated JPA class named in the +link{beanClassName,beanClassName} property (as with
    // Hibernate, this allows you to specify schema and mapping separately if you need to do
    // so).  JPA DataSource generation relies on annotations (the orm.xml mapping file is not
    // supported).  More information on JPA DataSources is +link{group:jpaIntegration,here}</li>
    // <li>For other DataSource types, attempt to find a Spring bean with the name specified in
    // the +link{schemaBean,schemaBean} property.  If no such bean is found (or Spring is not
    // present), attempt to instantiate an object whose fully-qualified class name is the value
    // in the <code>schemaBean</code> property.  If one of these approaches succeeds, we derive
    // the schema from the discovered object (by treating it as a Java Bean and assuming that
    // each one of its getters corresponds to a like-named field in the DataSource).  More
    // information on custom DataSource implementations is +link{group:writeCustomDataSource,here}.</li>
    // </ul>
    // The following table shows how SQL types are derived into
    // +link{type:FieldType,DataSource types} when we use an SQL table as a source of metadata
    // for a SQL or Hibernate DataSource:
    // <table border="1" class="normal">
    // <tr><th>SQL type</th><th>+link{attr:DataSourceField.type,DataSource type}</th></tr>
    // <tr><td>CHAR, VARCHAR, LONGVARCHAR, TEXT, CLOB</td><td>text</td></tr>
    // <tr><td>BIT, TINYINT, SMALLINT, INTEGER, BIGINT, DECIMAL<sup>*</sup>, NUMBER<sup>**</sup></td><td>integer</td></tr>
    // <tr><td>REAL, FLOAT, DOUBLE, DECIMAL<sup>*</sup>, NUMBER<sup>**</sup></td><td>float</td></tr>
    // <tr><td>DATE</td><td>date</td></tr>
    // <tr><td>TIME</td><td>time</td></tr>
    // <tr><td>TIMESTAMP</td><td>datetime</td></tr>
    // <tr><td>BLOB, BINARY, VARBINARY, LONGVARBINARY</td><td>binary</td></tr>
    // </table>
    // <sup>*</sup>For DECIMAL types, we inspect the "DECIMAL_DIGITS" attribute of the JDBC
    // metadata and designate the field type "integer" if that attribute is 0, or "float" if
    // it is some other value.<br>
    // <sup>**</sup>NUMBER is an Oracle-only type that appears in the JDBC metadata as DECIMAL
    // and is handled exactly the same way as DECIMAL
    // <p>
    // The following table shows how Java types are derived into DataSource types when we use
    // an unannotated Java class (Spring bean, Hibernate mapping or POJO) as a source of
    // metadata for a SQL, Hibernate or custom DataSource:
    // <table border="1" class="normal">
    // <tr><th>Java type</th><th>+link{attr:DataSourceField.type,DataSource type}</th></tr>
    // <tr><td>boolean, Boolean</td><td>boolean</td></tr>
    // <tr><td>char, Character, String, Reader</td><td>text</td></tr>
    // <tr><td>byte, short, int, long, Byte, Short, Integer, Long, BigInteger</td><td>integer</td></tr>
    // <tr><td>float, double, Float, Double, BigDecimal</td><td>float</td></tr>
    // <tr><td>Enum</td><td>enum (see discussion below)</td></tr>
    // <tr><td>InputStream</td><td>binary</td></tr>
    // <tr><td>java.sql.Date, java.util.Date, java.util.Calendar</td><td>date</td></tr>
    // <tr><td>java.sql.Time</td><td>time</td></tr>
    // <tr><td>java.sql.Timestamp</td><td>datetime</td></tr>
    // </table>
    // <p>
    // We also derive the following information for Hibernate mappings described in
    // <code>.hbm.xml</code> files (for Hibernate mappings described in Java annotations, see
    // the next table):
    // <ul>
    // <li>The first field we encounter that is described in the mapping with an &lt;id&gt; tag
    //     is marked as a primaryKey</li>
    // <li>If that field is marked as being generated, we set its type to "sequence"</li>
    // </ul>
    // Finally, this table shows how Java types are derived into DataSource types when we use an
    // annotated class as a source of metadata.  Note annotated classes are necessary for JPA
    // DataSources, but you can choose to use them for other types of DataSource as well.
    // For Hibernate DataSources, this is very worthwhile because Hibernate will also make use
    // of the annotations as config, meaning you don't need to specify <code>.hbm.xml</code>
    // files.  For SQL and custom DataSources, there is no benefit at the persistence level,
    // but it may still be worthwhile because the use of an annotated class gives us better
    // metadata and allows us to generate a better, closer-fitting autoDerive DataSource than
    // we can from examination of SQL schema or plain Java Beans:
    // <table border="1" class="normal">
    // <tr><th>Java type</th><th>+link{attr:DataSourceField.type,DataSource type}</th></tr>
    // <tr><td>boolean, Boolean</td><td>boolean</td></tr>
    // <tr><td>char, Character, String, Reader</td><td>text</td></tr>
    // <tr><td>byte, short, int, long, Byte, Short, Integer, Long, BigInteger</td><td>integer</td></tr>
    // <tr><td>float, double, Float, Double, BigDecimal</td><td>float</td></tr>
    // <tr><td>InputStream</td><td>binary</td></tr>
    // <tr><td>java.util.Date (with Temporal set to DATE), java.sql.Date</td><td>date</td></tr>
    // <tr><td>java.util.Date (with Temporal set to TIME), java.sql.Time</td><td>time</td></tr>
    // <tr><td>java.util.Date (with Temporal set to TIMESTAMP), java.util.Calendar, java.sql.Timestamp</td><td>datetime</td></tr>
    // <tr><td>Enum (with Enumerated set to STRING)</td><td>enum (see discussion below)</td></tr>
    // <tr><td>Enum (with Enumerated set to ORDINAL)</td><td>intEnum (see discussion below)</td></tr>
    // <tr><td>Field with Lob annotation</td><td>binary</td></tr>
    // <tr><td>Field with GeneratedValue annotation</td><td>sequence</td></tr>
    // </table>
    // <p><p>
    // <h4>enums and valueMaps</h4>
    // When we come across Java <code>Enum</code> properties in plain or annotated classes,
    // as well as setting the field type as noted in the above tables, we also generate a
    // valueMap for the field, based on the <code>Enum</code> members.
    // <p>
    // For cases where we generate a field of SmartClient type "enum" (see the above tables),
    // the keys of the valueMap are the result of calling <code>name()</code> on each member
    // of the underlying Java Enum (in other words, its value exactly as declared in its
    // enum declaration).  For cases where we generate a field of SmartClient type "intEnum",
    // the keys of the valueMap are strings representing the ordinal number of each member
    // in the Java Enum - "0", "1", etc. Note that this behavior will be overriden by
    // +link{attr:DataSource.enumTranslateStrategy,DataSource.enumTranslateStrategy} if both are set.
    // <p>
    // In both of these case, the display values generated for the valueMap are the result
    // of calling <code>toString()</code> on each Enum member.  If that gives the same
    // value as calling <code>name()</code>, the value is passed through
    // <code>DataTools.deriveTitleFromName()</code>, which applies the same processing rules
    // as +link{classMethod:DataSource.getAutoTitle()} to derive a more user-friendly display value.
    // <p>
    // <b>NOTE:</b> <code>schemaBean</code> implies <code>autoDeriveSchema</code>, because it
    // has no other purpose than to name the bean to use for auto-derived schema.  Thus, if you
    // specify <code>schemaBean</code> you do not need to specify <code>autoDeriveSchema</code>
    // as well (though it does no harm to do so).  However, <code>tableName</code> can be
    // validly specified without implying <code>autoDeriveSchema</code>, so in that case you
    // must explicitly specify <code>autoDeriveSchema</code>.
    // <p>
    // <b>NOTE:</b> The underlying super-DataSource is cached in server memory, so that it does
    // not have to be derived and created each time you need it.  However, the cache manager
    // will automatically refresh the cached copy if it detects that the deriving DataSource
    // has changed.  Thus, if you change the metadata your DataSource is deriving (if, for
    // example, you add a column to a table), all you need to do is touch the
    // <code>.ds.xml</code> file (ie, update its last changed timestamp - you don't actually
    // have to change it) and the cached copy will be refreshed next time it is needed.
    // <p>
    // When autoDeriveSchema is set, SQLDataSource will automatically discover foreignKeys and
    // deliver table and column name information to the client in hashed form so that two
    // DataSources that are linked by native SQL foreign keys will automatically discover each
    // other if loaded into the same application, and set dataSourceField.foreignKey
    // automatically.  Because the table and column names are delivered as cryptohashes, there
    // is no information leakage, but regardless the feature can be disabled via setting
    // datasource.autoLinkFKs to false in +link{group:server_properties,server.properties}.
    // This hashed linkage information is
    // delivered to the client in properties +link{dataSource.tableCode}
    // and +link{dataSourceField.fkTableCode}/+link{dataSourceField.fkColumnCode,fkColumnCode}
    //
    // @group fields
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.schemaBean (string : null : IR)
    // For DataSources that specify +link{autoDeriveSchema}, this property indicates the name
    // of the Spring bean, Hibernate mapping or fully-qualified Java class to use as parent
    // schema.
    //
    // @see autoDeriveSchema
    // @group fields
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.idClassName (string : null : IR)
    // For JPA and Hibernate DataSources this property indicates, that data source has composite primary key and
    // specifies fully-qualified Java class:<ul>
    // <li>with <b><code>@EmbeddedId</code></b> you have to specify class name of declared id</li>
    // <li>with <b><code>@IdClass</code></b> you have to specify class specified in annotation declaration</li></ul>
    //
    // @group fields
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.patternSingleWildcard (String | Array of String : ["?","%"] : IR)
    // When using the "matchesPattern", "iMatchesPattern", "containsPattern" or "iContainsPattern"
    // +link{type:OperatorId,search operator}, character that matches any single character.
    // <p>
    // Pass multiple strings to provide multiple alternative wildcards.
    // @visibility external
    //<
    patternSingleWildcard: ["?","%"],

    //> @attr dataSource.patternMultiWildcard (String | Array of String : "*" : IR)
    // When using the "matchesPattern", "iMatchesPattern", "containsPattern" or "iContainsPattern"
    // +link{type:OperatorId,search operator}, character that matches a series of one or more characters.
    // <p>
    // Pass multiple strings to provide multiple alternative wildcards.
    // @visibility external
    //<
    patternMultiWildcard: "*",

    //> @attr dataSource.patternEscapeChar (String : "\" : IR)
    // When using the "matchesPattern", "iMatchesPattern", "containsPattern" or "iContainsPattern"
    // +link{type:OperatorId,searchoperator}, character that escapes the
    // +link{patternSingleWildcard} or +link{patternMultiWildcard} if placed before it, so that
    // it is treated as a literal character.
    // @visibility external
    //<
    patternEscapeChar: "\\",

    //> @attr dataSource.translatePatternOperators (boolean : false : IR)
    // +link{type:OperatorId,Search operators} like "matchesPattern" use patterns like
    // "foo*txt" to match text values.  The patterns are similar to the patterns you use to
    // match names of files in a command-line interface, or to the pattern allowed for the SQL
    // "LIKE" operator.
    // <p>
    // <code>translatePatternOperators</code> controls whether these pattern operators should
    // be translated to a nested series of "startsWith"/"endsWidth"/"contains" operators before
    // being sent to the server.  This allows a server that only implements simple operators
    // like "startsWith" to support pattern operators such as "matchesPattern" and
    // "containsPattern", but with caveats:
    //    <ul>
    //        <li> single-character wildcards are not supported
    //        <li> multiple wildcards are not truly order-dependent, for example *1*2*3* will
    //             match 1,2,3 as interior characters in any order.
    //        <li> may be less efficient than a direct server-side implementation that is able to
    //             translate the pattern directly to the underlying storage engine.
    //    </ul>
    // <p>
    // Note that since "containsPattern" is essentially equivalent to "matchesPattern" but with
    // "*" wildcards at the beginning and end of every pattern, the second limitation (pattern
    // not really order dependent) may be fairly obvious to users when using this feature.  For
    // example, "m*t" will match "we meet" and "we teem".
    // <p>
    // The following are examples of how patterns are translated to simpler operators.  Note
    // that the case sensitive version of the operator is referred to below, but of course
    // "iMatchesPattern" and "iContainsPattern" will be translated to case-insensitive versions
    // of these operators, such as "iStartsWith".
    // <p>
    // *foo (endsWith)<br>
    // foo* (startsWith)<br>
    // *foo* (contains)<br>
    // *foo*bar (contains and endsWith)<br>
    // foo*bar* (startsWith and contains)<br>
    // foo*bar (startsWith and endsWith)<br>
    // *foo*bar* (multiple contains)
    // <p>
    // Also supported: one startsWith, multiple contains, one endsWith.
    // @visibility external
    //<
    translatePatternOperators: false,

    // XML
    // ---------------------------------------------------------------------------------------

    //> @attr dataSource.xmlNamespaces (Object : See below : IR)
    // Sets the XML namespace prefixes available for XPaths on a DataSource-wide basied.  See
    // +link{operationBinding.xmlNamespaces} for details.
    //
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    //<

    //> @attr dataSource.serviceNamespace (URN : null : IR)
    // For an XML DataSource, URN of the WebService to use to invoke operations.  This URN
    // comes from the "targetNamespace" attribute of the &lt;wsdl:definitions&gt; element in a
    // WSDL (Web Service Description Language) document, and serves as the unique identifier of
    // the service.
    // <P>
    // Having loaded a WebService using +link{XMLTools.loadWSDL()}, setting
    // <code>serviceNamespace</code> combined with specifying
    // +link{class:OperationBinding,operationBindings}
    // that set +link{attr:operationBinding.wsOperation} will cause a DataSource to invoke web
    // service operations to fulfill DataSource requests (+link{class:DSRequest,DSRequests}).
    // <P>
    // Setting <code>serviceNamespace</code> also defaults
    // +link{DataSource.dataURL,dataURL} to the service's location,
    // +link{DataSource.dataFormat,dataFormat} to "xml" and
    // +link{operationBinding.dataProtocol,dataProtocol} to "soap".
    //
    // @group wsdlBinding
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    // @example wsdlBinding
    //<

    //> @attr dataSource.schemaNamespace (URN : null : R)
    // For a DataSource derived from WSDL or XML schema, the XML namespace this schema belongs
    // to.  This is a read-only attribute automatically present on DataSources returned from
    // +link{schemaSet.getSchema()} and +link{webService.getSchema()}.
    //
    // @group wsdlBinding
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    //<

    //> @attr dataSource.recordXPath          (XPathExpression : null : [IR])
    // See +link{attr:operationBinding.recordXPath}.  <code>recordXPath</code> can be specified
    // directly on the DataSource for a simple read-only DataSource only capable of "fetch"
    // operations.
    //
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    // @example xmlDataSource
    // @example jsonXPath
    //<

    //> @attr dataSource.dataURL              (URL : null : [IR])
    // Default URL to contact to fulfill all DSRequests.  Can also be set on a
    // per-operationType basis via +link{attr:operationBinding.dataURL}.
    // <P>
    // NOTE: Best practice is to use the same <code>dataURL</code> for all DataSources which
    // fulfill DSRequests via the server-side RPCManager API.  Otherwise, cross-DataSource
    // +link{RPCManager.startQueue(),operation queuing} will not be possible.
    //
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    // @example jsonDataSource
    //<

    //> @attr dataSource.tagName             (String : null : IRA)
    // Tag name to use when serializing to XML.  If unspecified, the <code>dataSource.ID</code>
    // will be used.
    // @group clientDataIntegration
    // @serverDS allowed
    // @visibility xmlBinding
    //<


    //> @attr dataSource.useOfflineStorage (boolean : null : IRW)
    // Whether we store server responses for this DataSource into
    // +link{class:Offline,browser-based offline storage}, and then use those stored responses
    // at a later time if we are offline (ie, the application cannot connect to the server).
    // Note that by default we do NOT use offline storage for a dataSource.
    // @serverDS allowed
    // @group offlineGroup
    // @visibility external
    //<



// whether to transform XML responses to JS automatically, such that the client-side DSResponse
// is a JS object (whether the transform was performed on client or server is intended to be an
// implementation detail).
// At the moment 2005.7.5, some components can work directly with XML objects (eg read-only,
// selectable ListGrid), some can't.  Server XML->JS transform will always speed up the client.
// Aside from features that don't yet work with XML object, client XML->JS transform is a
// tradeoff between initial load vs later access time.
transformResponseToJS:true,

// whether this DataSource can queue requests to be submitted as a batch
supportsRequestQueuing : true,

    // Client Only DataSources
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.clientOnly     (Boolean : false : [IR])
    // A clientOnly DataSource simulates the behavior of a remote data store by manipulating a
    // static dataset in memory as +link{DSRequest,DSRequests} are executed on it.  Any changes
    // are lost when the user reloads the page or navigates away.
    // <P>
    // A clientOnly DataSource will return responses asynchronously, just as a DataSource
    // accessing remote data does.  This allows a clientOnly DataSource to be used as a
    // temporary placeholder while a real DataSource is being implemented - if a clientOnly
    // DataSource is replaced by a DataSource that accesses a remote data store, UI code for
    // components that used the clientOnly DataSource will not need be changed.
    // <P>
    // A clientOnly DataSource can also be used as a shared cache of modifiable data across
    // multiple UI components when immediate saving is not desirable.  In this case, several
    // components may interact with a clientOnly DataSource and get the benefit of
    // +link{ResultSet} behaviors such as automatic cache sync and in-browser data filtering
    // and sorting.  When it's finally time to save, +link{dataSource.cacheData} can be
    // inspected for changes and data can be saved to the original DataSource via
    // +link{addData()}, +link{updateData()} and +link{removeData()}, possibly in a
    // +link{rpcManager.startQueue,transactional queue}.  Note that
    // +link{DataSource.getClientOnlyDataSource()} lets you easily obtain a
    // <code>clientOnly</code> DataSource representing a subset of the data available from a
    // normal DataSource.
    // <P>
    // See also +link{cacheAllData} - a <code>cacheAllData</code> behaves like a write-through
    // cache, performing fetch and filter operations locally while still performing remote save
    // operations immediately.
    // <P>
    // ClientOnly DataSources can be populated programmatically via +link{cacheData} - see
    // +link{group:clientOnlyDataSources,this discussion} for other ways to populate a
    // client-only DataSource with data.
    //
    // @group clientOnlyDataSources
    // @serverDS allowed
    // @visibility external
    // @example localDataSource
    //<
    //clientOnly: false,

    // in clientOnly mode, whether to shallow copy results to more fully simulate a server.
    // This allows separate changes to be made to the "server data set" (testData) and
    // ResultSet caches.
    copyLocalResults:true,
    // Also support a mode where we explicitly clone the local-data. This will recursively
    // clone sub-objects. Off by default - not usually necessary and a little risky as
    // data may have references to non-serializable objects.
    deepCopyLocalResults:false,

    // Filtering
    // ----------------------------------------------------------------------------------------

    //> @attr dataSource.criteriaPolicy (CriteriaPolicy : "dropOnShortening" : IRWA)
    // Decides under what conditions the +link{ResultSet} cache should be dropped when the
    // +link{resultSet.criteria} changes.
    // @see compareCriteria()
    // @serverDS allowed
    // @visibility external
    //<
    criteriaPolicy:"dropOnShortening",

    //> @attr dataSource.defaultTextMatchStyle (TextMatchStyle : "exact" : [IR])
    // The default textMatchStyle to use for +link{DSRequest}s that do not explicitly state
    // a +link{dsRequest.textMatchStyle,textMatchStyle}.    Note, however, that DSRequests
    // issued by +link{ListGrid}s and other +link{DataBoundComponent,components} will
    // generally have a setting for textMatchStyle on the component itself (see
    // +link{ListGrid.autoFetchTextMatchStyle}, for example).
    //
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    // @example jsonDataSource
    // @example simpleJSON
    //<
    defaultTextMatchStyle:"exact",

    //> @attr dataSource.ignoreTextMatchStyleCaseSensitive (Boolean : false : [IR])
    // For fields on this dataSource that specify
    // +link{dataSourceField.ignoreTextMatchStyle,ignoreTextMatchStyle} true, the prevailing
    // textMatchStyle is ignored and SmartClient matches exact values.  This property dictates
    // whether that match is case-sensitive like the "exactCase" textMatchStyle, or
    // case-insensitive like the "exact" textMatchStyle (the default).  Please see the
    // +link{type:TextMatchStyle,TextMatchStyle documentation} for a discussion of the nuances
    // of case-sensitive matching.
    //
    // @serverDS allowed
    // @visibility external
    //<
    ignoreTextMatchStyleCaseSensitive:false,

    // ResultTrees
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.resultTreeClass (Class Object : null : [IRA])
    // Class for ResultTrees used by this datasource.  If null, defaults to using
    // +link{ResultTree}.
    // <P>
    // This can be set to a custom subclass of ResultTree that, for example, hangs on to extra
    // information necessary for integration with web services.
    //
    // @serverDS allowed
    //  @visibility external
    //<

    // ResultSets
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.resultSetClass (Class Object : null : [IRA])
    // Class for ResultSets used by this datasource.  If null, defaults to using
    // +link{ResultSet}.
    // <P>
    // This can be set to a custom subclass of ResultSet that, for example, hangs onto to extra
    // information necessary for integration with web services.
    //
    // @serverDS allowed
    // @visibility external
    //<

    // Validation
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.useLocalValidators (boolean : null : IRWA)
    // Whether to attempt validation on the client at all for this DataSource.  If unset (the
    // default), client-side validation is enabled.
    // <p>
    // Disabling client-side validation entirely is a good way to test server-side validation.
    //
    // @group validation
    // @serverDS allowed
    // @visibility external
    //<
    // NOTE: code that checks this property interprets null as true

    //> @attr dataSource.requiredMessage (string : null : [IRW])
    // The required message when a field that has been marked as
    // +link{DataSourceField.required,required} is not filled in by the user.
    // @group formTitles
    // @visibility external
    //<

    // ShowPrompt
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.showPrompt (Boolean : true : IRW)
    // Whether RPCRequests sent by this DataSource should enable
    // +link{attr:RPCRequest.showPrompt} in order to block user interactions until the
    // request completes.
    // <p>
    // DataSource requests default to blocking UI interaction because, very often, if the user
    // continues to interact with an application that is waiting for a server response, some
    // kind of invalid or ambiguous situation can arise.
    // <p>
    // Examples include pressing a "Save" button a second time before the first save completes,
    // making further edits while edits are still being saved, or trying to initiate editing on
    // a grid that hasn't loaded data.
    // <p>
    // Defaulting to blocking the UI prevents these bad interactions, or alternatively, avoids
    // the developer having to write repetitive code to block invalid interactions on every
    // screen.
    // <p>
    // If an operation should ever be non-blocking, methods that initiate DataSource requests
    // (such as +link{DataSource.fetchData()}) will generally have a
    // <code>requestProperties</code> argument allowing <code>showPrompt</code> to be set to
    // false for a specific request.
    //
    // @serverDS allowed
    // @visibility external
    //<
    showPrompt:true,

    // Selection
    // ----------------------------------------------------------------------------------------
    //> @attr dataSource.selectionClass (className : null : [A])
    // Class to use as the Selection object for ResultSets derived from this DataSource.
    // @visibility serverSelection
    //<


    // Java enum translation
    // ----------------------------------------------------------------------------------------
    //> @type EnumTranslateStrategy
    // Determines how Java enums are translated to and from Javascript by the SmartClient server.
    //
    // @value "name"
    //   Translates to/from a String matching the constant name. This is the default if not set.
    // @value "string"
    //   Translates to/from a String matching the <code>enum.toString()</code>.
    // @value "ordinal"
    //   Translates to/from an integer matching the ordinal number of the constant within
    //   the enumeration
    // @value "bean"
    //   Translates to/from a Javascript object containing one property for each property defined
    //   within the enum. The constant itself and the ordinal number are included in the JS object.
    //   By default they are called "_constant" and "_ordinal", but this can be overridden with
    //   the +link{dataSource.enumOrdinalProperty} and +link{dataSource.enumConstantProperty}
    //   properties
    //
    // @see dataSource.enumTranslateStrategy
    // @serverDS allowed
    // @visibility external
    //<


    //> @attr dataSource.enumTranslateStrategy (EnumTranslateStrategy : null : IA)
    //
    // Sets the strategy this DataSource uses to translate Java enumerated types (objects of type
    // enum) to and from Javascript.
    //
    // This property is only applicable if you are using the SmartClient server
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.enumConstantProperty (String : null : IA)
    //
    // The name of the property this DataSource uses for constant name when translating
    // Java enumerated types to and from Javascript, if the +link{enumTranslateStrategy} is set
    // to "bean".  Defaults to "_constant" if not set.
    // <p>
    //  This property is only applicable if you are using the SmartClient server
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.enumOrdinalProperty (String : null : IA)
    //
    // The name of the property this DataSource uses for ordinal number when translating
    // Java enumerated types to and from Javascript, if the +link{enumTranslateStrategy} is set
    // to "bean".  Defaults to "_ordinal" if not set.
    // <p>
    //  This property is only applicable if you are using the SmartClient server
    // @serverDS allowed
    // @visibility external
    //<

    //> @attr dataSource.autoDeriveTitles (boolean : true : IR)
    // If set, titles are automatically derived from +link{dataSourceField.name,field.name} for any
    // field that does not have a +link{dataSourceField.title,field.title} and is not marked
    // +link{dataSourceField.hidden,hidden}:true, by calling the method +link{getAutoTitle()}.
    // @serverDS allowed
    // @visibility external
    //<
    autoDeriveTitles: true,

    // Multi-level Sorting
    //> @attr dataSource.canMultiSort (boolean : true : IR)
    // When true, indicates that this DataSource supports multi-level sorting.
    // @serverDS allowed
    // @visibility external
    //<
    canMultiSort: true,

    //> @attr dataSource.sparseUpdates (boolean : false : IR)
    // When true, indicates that any updates for this DataSource include only those fields
    // that have actually changed (and primaryKey fields); when false (the default), all
    // field values are included in updates, whether they have changed or not
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.noNullUpdates (boolean : false : IR)
    // When true, indicates that fields in this DataSource will never be positively updated
    // to the null value; they may arrive at null values by being omitted, but we will
    // not send actual null values in update requests.  When false (the default), null is
    // not treated in any special way.
    // <p>
    // Setting this value causes null-assigned fields to be replaced with the field's
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}, if one is declared.
    // If no <code>nullReplacementValue</code> is declared for the field, the null assignment
    // is replaced with the DataSource's +link{nullStringValue,nullStringValue},
    // +link{nullIntegerValue,nullIntegerValue}, +link{nullFloatValue,nullFloatValue}
    // or +link{nullDateValue,nullDateValue}, depending on the field type.
    // <p>
    // For "add" operations, setting +link{omitNullDefaultsOnAdd,omitNullDefaultsOnAdd} causes
    // null-valued fields to be removed from the request entirely, rather than replaced with
    // default values as described above.
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.omitNullDefaultsOnAdd (boolean : false : IR)
    // When true, and +link{noNullUpdates,noNullUpdates} is also true, indicates that "add"
    // requests on this DataSource will have null-valued fields removed from the request
    // entirely before it is sent to the server, as opposed to the default behavior of
    // replacing such null values with defaults.
    // @see noNullUpdates,noNullUpdates
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.nullStringValue (String : "" : IR)
    // If +link{noNullUpdates} is set, the value to use for any text field that has a null
    // value assigned on an update operation, and does not specify an explicit
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}.
    // @see noNullUpdates,noNullUpdates
    // @see DataSourceField.nullReplacementValue
    // @serverDS only
    // @visibility external
    //<
    nullStringValue: "",

    //> @attr dataSource.nullIntegerValue (int : 0 : IR)
    // If +link{noNullUpdates} is set, the value to use for any integer field that has a null
    // value assigned on an update operation, and does not specify an explicit
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}.
    // @see noNullUpdates,noNullUpdates
    // @see DataSourceField.nullReplacementValue
    // @serverDS only
    // @visibility external
    //<
    nullIntegerValue: 0,

    //> @attr dataSource.nullFloatValue (float : 0.0 : IR)
    // If +link{noNullUpdates} is set, the value to use for any float field that has a null
    // value assigned on an update operation, and does not specify an explicit
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}.
    // @see noNullUpdates,noNullUpdates
    // @see DataSourceField.nullReplacementValue
    // @serverDS only
    // @visibility external
    //<
    nullFloatValue: 0.0,

    //> @attr dataSource.nullBooleanValue (boolean : false : IR)
    // If +link{noNullUpdates} is set, the value to use for any boolean field that has a null
    // value assigned on an update operation, and does not specify an explicit
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}.
    // @see noNullUpdates,noNullUpdates
    // @see DataSourceField.nullReplacementValue
    // @serverDS only
    // @visibility external
    //<
    nullBooleanValue: false,

    //> @attr dataSource.nullDateValue (Date : See below : IR)
    // If +link{noNullUpdates} is set, the value to use for any date or time field that has a
    // null value assigned on an update operation, and does not specify an explicit
    // +link{DataSourceField.nullReplacementValue,nullReplacementValue}.
    // <p>
    // Unlike strings and numbers, there is no "natural" choice for a null replacement value
    // for dates.  The default value we have chosen is midnight on January 1st 1970, simply
    // because this is "the epoch" - the value that is returned by calling "new Date(0)"
    // @see noNullUpdates,noNullUpdates
    // @see DataSourceField.nullReplacementValue
    // @serverDS only
    // @visibility external
    //<
    nullDateValue: new Date(0),


    // Role-based security
    // ----------------------------------------------------------------------------------------

    //> @attr dataSource.requiresAuthentication (boolean : null : IR)
    // Whether a user must be authenticated in order to access this DataSource.  This establishes a
    // default for the DataSource as a whole; individual +link{operationBindings} within the
    // DataSource may still override this setting by explicitly setting
    // +link{operationBinding.requiresAuthentication}.
    // <P>
    // Whether the user is authenticated is determined by calling
    // <code>httpServletRequest.getRemoteUser()</code>, hence works with both simple J2EE security
    // (realms and form-based authentication) and JAAS (Java Authentication & Authorization
    // Service).
    // <P>
    // If you wish to use an authentication scheme that does not make use of the servlet API's
    // standards, SmartClient Server also implements the <code>setAuthenticated</code> method
    // on <code>RPCManager</code>.  You can use this API to tell SmartClient that all the
    // requests in the queue currently being processed are associated with an authenticated
    // user; in this case, SmartClient will not attempt to authenticate the user via
    // <code>httpServletRequest.getRemoteUser()</code>
    // <P>
    // You can set the default value for this property via setting "authentication.defaultRequired"
    // in +link{group:server_properties,server.properties}.  This allows you to, for example,
    // cause all DataSources to require
    // authentication for all operations by default.
    // <P>
    // Note that setting this property does not automatically cause an authentication mechanism to
    // appear - you still need to separately configure an authentication system.  Likewise, setting
    // requiresAuthentication="false" does not automatically allow users to bypass your authentication
    // mechanism - you need to set up a URL that will accept DSRequests and process them similar to
    // the default "IDACall" servlet, and which is not protected by the authentication system.  See
    // +link{group:servletDetails,Deploying SmartClient} for details on the IDACall servlet.
    //
    // @requiresModules SCServer
    // @group auth
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.requiresRole (String : null : IR)
    // Similar to +link{operationBinding.requiresRole}, but controls access to the DataSource as a
    // whole.
    //
    // @requiresModules SCServer
    // @group auth
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.requires (VelocityExpression : null : IR)
    // Indicates that the specified +link{type:VelocityExpression} must evaluate to true for a user
    // to access this DataSource.
    // <P>
    // See also +link{operationBinding.requires}.
    //
    // @requiresModules SCServer
    // @group auth
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.creatorOverrides (boolean : null : IR)
    // Indicates that declarative security rules are waived for rows that were created by the
    // current user.  Practically, this means that when a security check fails, instead of a
    // security exception being thrown, we alter the criteria to ensure that the request can
    // only return or affect rows that were created by the current authenticated user.  This
    // allows you to create security regimes whereby users can see and edit data they created,
    // but have access to other users' data forbidden or limited.
    // <p>
    // In order for this to work, we require two things:<ul>
    // <li>The DataSource must specify a field of type "creator" - this field type is described
    //     on +link{type:FieldType,this page}</li>
    // <li>The authentication regime in use must include the idea of a "current user".  The
    //     authentication provided by the Servlet API is an example of such a regime.</li>
    // </ul>
    // This setting can be overridden at operationBinding and field level, allowing extremely
    // fine-grained control.
    //
    // @requiresModules SCServer
    // @see operationBinding.creatorOverrides
    // @see dataSourceField.creatorOverrides
    // @group fieldLevelAuth
    // @serverDS only
    // @visibility external
    //<


    // Transactions
    // ----------------------------------------------------------------------------------------

    //> @attr dataSource.autoJoinTransactions (boolean : null : IR)
    // If true, causes all operations on this DataSource to automatically start or join a
    // transaction; this means that multiple operations sent to the server in a
    // +link{RPCManager.startQueue(),request queue} will be committed in a single transaction.
    // Note that this includes fetch operations - setting this property to true has the same
    // effect as a transaction policy of ALL for just this DataSource's operations - see the
    // server-side <code>RPCManager.setTransactionPolicy()</code> for details of the different
    // TransactionPolicy settings.
    // <p>
    // If this property is explicitly false, this causes all operations on this DataSource
    // to be committed individually - the same as a transaction policy of NONE, just for this
    // DataSource's operations.
    // <p>
    // In either case, you can override the setting for individual operations - see
    // +link{OperationBinding.autoJoinTransactions}.
    // <P>
    // If this property if null or not defined, we fall back to the default setting for this
    // type of DataSource.  These are defined in +link{group:server_properties,server.properties}
    // as follows:
    // <ul>
    // <li><b>Hibernate:</b><code> hibernate.autoJoinTransactions</code></li>
    // <li><b>JPA/JPA2:</b><code> jpa.autoJoinTransactions</code></li>
    // <li><b>SQL:</b> There is one setting per configured database connection (+link{dbName,dbName}).
    // For example, the setting for the default MySQL connection is
    // <code> sql.Mysql.autoJoinTransactions</code></li>
    // </ul>
    // If the setting is not defined at the DataSource-type level, we use the system global
    // default, which is defined in <code>server.properties</code> as
    // <code>autoJoinTransactions</code>.
    // <P>
    // At the dbName and global system levels, you can set the autoJoinTransactions attribute
    // to a valid Transaction Policy, rather than a simple true or false (although these
    // values work too - true is the same as ALL, false is the same as NONE).  For valid
    // TransactionPolicy values and their meanings, see the server-side Javadoc for
    // <code>RPCManager.setTransactionPolicy()</code>
    // <P>
    // Note that the configuration settings discussed here can be overridden for a particular
    // queue of requests by setting the server-side RPCManager's transaction policy.  Look in
    // the server-side Javadoc for <code>RPCManager.getTransactionPolicy()</code>.
    // <P>
    // NOTE: Setting this property to true does not cause a transactional persistence
    // mechanism to automatically appear - you have to ensure that your DataSource supports
    // transactions.  The SmartClient built-in SQL, Hibernate and JPA DataSources support transactions,
    // but note that they do so <b>at the provider level</b>.  This means that you can combine
    // updates to, say, an Oracle database and a MySQL database in the same queue, but they
    // will be committed in <em>two</em> transactions - one per database.  The SmartClient
    // server will commit or rollback these two transactions as if they were one, so a
    // failure in some Oracle update would cause all the updates to both databases to be
    // rolled back.  However, this is not a true atomic transaction; it is possible for one
    // transaction to be committed whilst the other is not - in the case of hardware failure,
    // for example.
    // <p>
    // NOTE: Not all the supported SQL databases are supported for transactions.  Databases
    // supported in this release are:
    // <ul>
    // <li>DB2</li>
    // <li>HSQLDB</li>
    // <li>Firebird</li>
    // <li>Informix</li>
    // <li>Microsoft SQL Server</li>
    // <li>MySQL (you must use InnoDB tables; the default MyISAM storage engine does not
    // support transactions)</li>
    // <li>Oracle</li>
    // <li>PostgreSQL</li>
    // </ul>
    //
    // @serverDS only
    // @see OperationBinding.autoJoinTransactions
    // @visibility transactions
    //<

    // SQL Paging Strategy
    // ----------------------------------------------------------------------------------------
    //> @type SQLPagingStrategy
    // The technique SmartClient Server's SQL DataSource should use to select a "page" of data
    // from a table.
    //
    // @value "sqlLimit"
    //   Specify the paging directly in the SQL query we generate.  The way this is done varies
    // considerably from database to database: with some it is a straightforward built-in
    // facility while others require arcane tricks or simply don't support the idea.  This is
    // the most efficient method, where available.  Note that this strategy is not supported
    // for operations that make use of a +link{operationBinding.customSQL,customSQL} clause,
    // because it depends upon being able to determine the size of the whole dataset without
    // actually retrieving the whole dataset.  Ordinary operations do this by means of an
    // automatically-generated "row count query", but we cannot generate such a query for a
    // <code>customSQL</code> operation.
    //
    // @value "jdbcScroll"
    //   Implement the paging behavior by use of the <code>absolute()</code> method of the
    // JDBC <code>ResultSet</code>.
    //
    // @value "dropAtServer"
    //   Implement the paging behavior by fetching the entire resultset from the database and
    // dropping any unnecessary rows on the server before returning the data to the client.
    // This approach is extremely inefficient, but also extremely straightforward; it is
    // intended as a fallback option, for situations where the more sophisticated approaches
    // cause problems (a JDBC driver that throws vague exceptions when <code>absolute()</code>
    // is called, for example)
    //
    // @value "none"
    //   No paging behavior: we always return the entire resultset to the client.
    //
    // @see dataSource.sqlPaging
    // @see operationBinding.sqlPaging
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.sqlPaging (SQLPagingStrategy : null : IRW)
    // The paging strategy to use for this DataSource.  If this property is not set, the
    // default paging strategy, specified with the +link{group:server_properties,server.properties}
    // setting <code>sql.defaultPaging</code>, is used.
    // <p>
    // This setting can be overridden with the +link{OperationBinding.sqlPaging} property.
    // <P>
    // <b>NOTE:</b> Operations that involve a +link{operationBinding.customSQL,customSQL}
    // clause ignore this property, because customSQL operations usually need to be treated
    // as special cases.  For these operations, the paging strategy comes from the
    // +link{group:server_properties,server.properties} setting
    // <code>sql.defaultCustomSQLPaging</code> or
    // <code>sql.defaultCustomSQLProgressivePaging</code>, depending on whether or not
    // +link{dataSource.progressiveLoading,progressiveLoading} is in force.  Note that these
    // can always be overridden by a <code>sqlPaging</code> setting on the OperationBinding.
    //
    // @see operationBinding.sqlPaging
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.sqlUsePagingHint (boolean : null : IR)
    // If explicitly set true or left null, causes the server to use a "hint" in the SQL we
    // generate for paged queries.  If explicitly set false, forces off the use of hints.  This
    // property can be overridden per operationBinding - see
    // +link{operationBinding.sqlUsePagingHint}.
    // <p>
    // Note this property is only applicable to +link{dataSource.serverType,SQL} DataSources,
    // only when a +link{dataSource.sqlPaging,paging strategy} of "sqlLimit" is in force,
    // and it only has an effect for those specific database products where we employ a native
    // hint in the generated SQL in an attempt to improve performance.
    //
    // @requiresModules SCServer
    // @see operationBinding.sqlUsePagingHint
    // @group sqlPaging
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSource.useSubselectForRowCount (boolean : null : IR)
    // This property is only applicable to +link{dataSource.serverType,SQL} DataSources, and
    // only for +link{class:OperationBinding,operations} that express a
    // +link{operationBinding.customSQL,customSQL} clause.  In these circumstances, we
    // generally switch off paging because we are unable to generate a "row count" query that
    // tells the framework the size of the complete, unpaged resultset.
    // <p>
    // The <code>useSubselectForRowCount</code> flag causes the framework to derive a rowcount
    // query by simply wrapping the entire customSQL clause in a subselect, like so:<code><br>
    // &nbsp;&nbsp;&nbsp;&nbsp;SELECT COUNT(*) FROM ({customSQL clause here})</code>
    // <p>
    // However, this is not guaranteed to give good results.  Because the customSQL clause can
    // contain anything that you can write in SQL, running it inside a subselect in order to
    // count the rows might not work, might have unintended side-effects (if, for example, it
    // is a stored procedure call that performs updates as part of its function), or it might
    // just be a bad idea - for example, if the customSQL query is slow-running, you'll make
    // it twice as slow with this flag, simply because you'll be running it twice.  We
    // recommend using this flag with care.
    // <p>
    // NOTE: This setting can be overridden per-operation - see
    // +link{operationBinding.useSubselectForRowCount}.  You can also set a global default for
    // this setting, so you don't have to specify it in every dataSource - define
    // <code>useSubselectForRowCount</code> as true in your
    // +link{group:server_properties,server.properties} file.
    //
    // @requiresModules SCServer
    // @group sqlPaging
    // @serverDS only
    // @visibility external
    //<


    // SQL autoDeriveSchema auto-discovered FK relations
    // ----------------------------------------------------------------------------------------

    //> @attr dataSource.tableCode (string : null : R)
    // <b>Only applicable to the built-in SQL DataSource</b>
    // <p>
    // This read-only attribute is a secure and unique hashed version of the name of the table
    // underlying this dataSource.  It is used automatically by client-side framework code to
    // link dataSources together by +link{dataSourceField.foreignKey,foreign key}, by matching
    // DataSource <code>tableCode</code>s with +link{dataSourceField.fkTableCode}s.  This is
    // necessary if we automatically discovered a foreign key relationship by inspecting the
    // database schema, as a result of a DataSource specifying +link{autoDeriveSchema,autoDeriveSchema}:
    // true.  We use a secure hash rather than the table name itself for security reasons -
    // sending the actual table name to the client would be considered an information leakage
    // that could encourage attempts at injection attacks.
    //
    // @requiresModules SCServer
    // @see dataSourceField.fkTableCode
    // @see dataSourceField.fkColumnCode
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSourceField.fkTableCode (string : null : R)
    // <b>Only applicable to the built-in SQL DataSource</b>
    // <p>
    // This read-only attribute is a secure and unique hashed version of the name of the table
    // underlying a dataSource that this dataSourceField is a
    // +link{dataSourceField.foreignKey,foreign key} to.  It is used automatically by client-side
    // framework code to link dataSources together by foreign key, by matching
    // <code>fkTableCode</code>s with +link{dataSource.tableCode}s.  This is necessary if we
    // automatically discovered a foreign key relationship by inspecting the database schema, as
    // a result of a DataSource specifying +link{dataSource.autoDeriveSchema,autoDeriveSchema}:
    // true.  We use a secure hash rather than the table name itself for security reasons -
    // sending the actual table name to the client would be considered an information leakage
    // that could encourage attempts at injection attacks.
    //
    // @requiresModules SCServer
    // @see dataSource.tableCode
    // @see dataSourceField.fkColumnCode
    // @serverDS only
    // @visibility external
    //<

    //> @attr dataSourceField.fkColumnCode (string : null : R)
    // <b>Only applicable to the built-in SQL DataSource</b>
    // <p>
    // This read-only attribute is a secure and unique hashed version of the name of the matching
    // column in the table underlying a dataSource that this dataSourceField is a
    // +link{dataSourceField.foreignKey,foreign key} to.  It is used automatically by client-side
    // framework code to link dataSources together by foreign key.  This is necessary if we
    // automatically discovered a foreign key relationship by inspecting the database schema, as
    // a result of a DataSource specifying +link{dataSource.autoDeriveSchema,autoDeriveSchema}:
    // true.  We use a secure hash rather than the column name itself for security reasons -
    // sending the actual column name to the client would be considered an information leakage
    // that could encourage attempts at injection attacks.
    //
    // @requiresModules SCServer
    // @see dataSource.tableCode
    // @see dataSourceField.fkTableCode
    // @serverDS only
    // @visibility external
    //<


    // progressiveLoading
    // ----------------------------------------------------------------------------------------

    //> @groupDef progressiveLoading
    // Settings related to the "progressive loading" pattern, which causes data to be loaded
    // progressively, so that users can request more rows by scrolling off the bottom of the
    // currently visible rows, but cannot cause direct movement to some arbitrary position in
    // the dataset (as is the case with ordinary, non-progressive loading).  This pattern may
    // give a better user experience with large datasets.
    // @visibility external
    //<

    //> @attr dataSource.progressiveLoading (boolean : null : IRW)
    // If true, causes SmartClient Server to use the "progressive loading" pattern for
    // fetches on this dataSource, as described in the <b>Paging and total dataset length</b>
    // section of the +link{ResultSet,ResultSet documentation}.  Essentially, this means that
    // we avoid issuing a row count query and instead advertise total rows as being slightly
    // more than the number of rows we have already read (see +link{endGap,endGap}).  This
    // allows users to load more of
    // a dataset by scrolling past the end of the currently-loaded rows, but it prevents them
    // from scrolling directly to the end of the dataset.
    // <p>
    // Generally, progressive loading is appropriate when you have to deal with very large
    // datasets. Note that by default, a dataSource will switch into progressive loading mode
    // automatically when it detects that it is dealing with a dataset beyond a certain size -
    // see +link{progressiveLoadingThreshold}.
    // <p>
    // This setting can be overridden for individual fetch operations with the
    // +link{OperationBinding.progressiveLoading} property, and also at the level of the
    // individual +link{DSRequest.progressiveLoading,DSRequest}.  You can also specify
    // <code>progressiveLoading</code> on
    // +link{DataBoundComponent.progressiveLoading,DataBoundComponents} and certain types of
    // <code>FormItem</code> - +link{SelectItem.progressiveLoading,SelectItem} and
    // +link{ComboBoxItem.progressiveLoading,ComboBoxItem}.
    // <p>
    // Currently, this property only applies to users of the built-in SQLDataSource, but you
    // could use it in custom DataSource implementations to trigger the server behavior
    // described in the <code>ResultSet</code> documentation linked to above.
    //
    // @see operationBinding.progressiveLoading
    // @see dataSource.progressiveLoadingThreshold
    // @see dataSource.lookAhead
    // @see dataSource.endGap
    // @serverDS allowed
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr dataSource.progressiveLoadingThreshold (int : 200000 : IRW)
    // Indicates the dataset size that will cause SmartClient Server to automatically
    // switch into +link{progressiveLoading,progressive loading mode} for this DataSource.
    // To prevent automatic switching to progressive loading, set this property to -1.
    //
    // @see dataSource.progressiveLoading
    // @serverDS only
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr dataSource.lookAhead (int : 1 : IRW)
    // If we are +link{progressiveLoading,loading progressively}, indicates the number of
    // extra records SmartClient Server will read beyond the end record requested by the client,
    // in order to establish if there are more records to view.  This property has no effect
    // if we are not progressive-loading.
    // <p>
    // This property can be tweaked in conjunction with +link{endGap,endGap} to change behavior
    // at the end of a dataset.  For example, with the default values of <code>lookAhead: 1</code>
    // and <code>endGap: 20</code>, we can end up with the viewport shrinking if we get a case
    // where there really was only one more record (because the client was initially told there
    // were 20 more).  This is not a problem per se, but it may be surprising to the user.
    // You could prevent this happening (at the cost of larger reads) by setting
    // <code>lookAhead</code> to be <code>endGap+1</code>.
    //
    // @see dataSource.progressiveLoading
    // @see dataSource.endGap
    // @serverDS only
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr dataSource.endGap (int : 20 : IRW)
    // If we are +link{progressiveLoading,loading progressively}, indicates the number of
    // extra records SmartClient Server will advertise as being available, if it detects that
    // there are more records to view (see +link{lookAhead,lookAhead}).  This property has no
    // effect if we are not progressive-loading.
    //
    // @see dataSource.progressiveLoading
    // @see dataSOurce.lookAhead
    // @serverDS only
    // @group progressiveLoading
    // @visibility external
    //<


    // cacheAllData implementation
    // ----------------------------------------------------------------------------------------

    //> @attr dataSource.cacheAllData (Boolean : null : IRW)
    // Set this property to true to have a DataSource fetch all of its data client-side on the
    // first fetch request.  However, unlike a +link{dataSource.clientOnly,clientOnly} DataSource, this DataSource
    // will still save changes normally, sending remote requests.
    // <P>
    // You can manually set this attribute after initialization by calling
    // +link{setCacheAllData}; setting +link{autoCacheAllData}:true causes a DataSource to
    // automatically switch to <code>cacheAllData:true</code> when a fetch results in the
    // entire dataset being brought client-side.
    // <P>
    // To cause automatic cache updates, you can set +link{cacheMaxAge} to a number of seconds
    // and once data has been client-side for that length of time, the next fetch causes the
    // cache to be dropped and a new cache retrieved.
    // <P>
    // Note that multiple +link{operationBindings} of type "fetch" which return distinct
    // results will not work with <code>cacheAllData</code>: only one cache is created and is
    // used for all fetch operations, regardless of whether +link{dsRequest.operationId} has
    // been set.  However, "fetch" operationBindings used as a
    // +link{operationBinding.cacheSyncOperation} will work normally, so long as they return
    // all data fields that are returned by the default "fetch" operation, so that the cache
    // can be updated.
    // <P>
    // To specify which operationId to use for fetching all data, use
    // +link{dataSource.cacheAllOperationId, cacheAllOperationId}.
    // <p>
    // To use the cache only for requests that have the <code>cacheAllOperationId</code>,
    // allowing any other operationId (or absence of an operationId) to contact the server as
    // normal, set +link{dataSource.cacheAcrossOperationIds,cacheAcrossOperationIds}.
    //
    // @setter setCacheAllData
    // @group clientData
    // @visibility external
    //<

    //> @attr dataSource.cacheAllOperationId (String : null : IR)
    // +link{dsRequest.operationId} to use for fetching data in case +link{dataSource.cacheAllData, cacheAllData} is
    // true.  By default a standard fetch operation is used (with no <code>operationId</code>
    // specified).
    // @group clientData
    // @visibility external
    //<

    //> @attr dataSource.cacheAcrossOperationIds (Boolean : true : IR)
    // When +link{dataSource.cacheAllData, cacheAllData} mode is enabled and a +link{dataSource.cacheAllOperationId} has been set,
    // this flag affects whether cached results are used for all "fetch" requests regardless of
    // their +link{dsRequest.operationId}, or are used only for "fetch" requests that use the
    // <code>cacheAllOperationId</code>, allowing other requests to go to server normally.
    // <p>
    // Default of <code>true</code> means that the <code>cacheAllOperationId</code> will be
    // used to fetch all rows, but the resulting cache will be used for all "fetch" operations
    // regardless of the <code>operationId</code> specified on the request.
    // <p>
    // Switching to "false" effectively creates caching just for one specific
    // <code>operationId</code> - the <code>cacheAllOperationId</code> - while allowing all
    // other requests to go to the server normally.
    //
    // @group clientData
    // @visibility external
    //<
    cacheAcrossOperationIds : true,

    //> @method dataSource.setCacheAllData()
    // Call this method to switch cacheAllData on or off after initialization.  Passing a
    // <code>shouldCache</code> value of false clears any existing client-side cache,
    // cancels any outstanding requests for a full cache and issues any other pending requests
    // normally.
    //
    // @param shouldCache (Boolean) New value for +link{cacheAllData}
    // @group clientData
    // @visibility external
    //<
    setCacheAllData : function (shouldCache) {
        if (!shouldCache) {
            if (this.cacheAllData == true) {
                if (this.logIsInfoEnabled("cacheAllData")) {
                    this.logInfo("setCacheAllData(false): clearing the cache and any "+
                        "deferred requests", "cacheAllData");
                }
                // TODO: reset to normal operation
                delete this._autoCacheAllData_timestamp;
                // 1) clear the cache if there is one
                this.invalidateCache();
                this.cacheAllData = false;
                // 2) cancel and ignore any outstanding "fetch" requests for a full cache
                this.clearDeferredRequests();

                if (this._insulatedTransforms == true) {
                    // remove these custom methods - sendDSRequest() has logic that differs for
                    // clientCustom requests if _isServerRequest is present
                    delete this._insulatedTransforms;
                    delete this._isServerRequest;
                    if (this.transformServerRequest) {
                        this.transformRequest = this.transformServerRequest;
                        delete this.transformServerRequest;
                    }
                    if (this.transformServerResponse) {
                        this.transformResponse = this.transformServerResponse;
                        delete this.transformServerResponse;
                    }
                }
            }
        } else {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("setCacheAllData(true): invalidate the cache", "cacheAllData");
            }
            this.cacheAllData = true;
            this._autoCacheAllData_timestamp = isc.timestamp();
            this.invalidateCache();
        }
    },

    //> @attr dataSource.cacheMaxAge (Number : 60 : IRW)
    // The maximum time, in seconds, to maintain the client-side cache.  If a fetch occurs after
    // the cacheMaxAge has expired, the current cache will be dropped and another complete
    // cache fetched.
    // @group clientData
    // @visibility external
    //<
    cacheMaxAge: 60,
    // cacheLastFetchTime - the time at which the client-side cache became valid - this value +
    // the cacheMaxAge determines when the cache will be invalidated.
    cacheLastFetchTime: 0,

    cacheNeedsRefresh : function () {
        var currentTime = new Date().getTime(),
            elapsedSeconds = ((currentTime - this.cacheLastFetchTime) / 1000),
            result = (this.cacheLastFetchTime == 0 || elapsedSeconds > this.cacheMaxAge)
        ;

        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("cacheNeedsRefresh returns "+result, "cacheAllData");
        }

        return result;
    },

    //> @attr dataSource.cacheData (Array of Record : null : IRW)
    // For a +link{cacheAllData} or client-only DataSource, a set of records to use as a dataset,
    // specified as an Array of JavaScript Objects representing records.
    // @setter setCacheData
    // @group clientData
    // @visibility external
    //<

    //> @method dataSource.setCacheData()
    // Call this method to set the data in the client-side cache after initialization.
    // @param data (Array of Record) Array of records to apply as the client-side cache
    // @group clientData
    // @visibility external
    //<
    setCacheData : function (data) {
        if (this.cacheAllData || this.clientOnly) {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("setCacheData: invalidating the cache", "cacheAllData");
            }
            // invalidate the cache and clear any pending requests
            this.invalidateCache();
            this.clearDeferredRequests();

            // set the cacheData

            this.cacheData = this.testData = data;

            // At this point our cacheData is fresh - reset the timer so the 'needsRefresh' method
            // returns false and doesn't ignore it!
            this.cacheLastFetchTime = new Date().getTime();

            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("setCacheData: cacheData has been set", "cacheAllData");
            }
        }
    },

    //> @attr dataSource.testData (Array of Record : null : IRW)
    // For a client-only DataSource, a set of records to use as a dataset, specified as an
    // Array of JavaScript Objects.
    // <P>
    // See +link{group:clientOnlyDataSources,this discussion} for ways to populate a
    // client-only DataSource with test data.
    // @setter setTestData
    // @group clientData
    // @visibility external
    // @deprecated In favor of +link{dataSource.cacheData}.
    //<

    //> @method dataSource.setTestData()
    // Call this method to set the data in the client-side test-data after initialization.
    // +link{setCacheData(),setCacheData()} should be called instead and setTestData() is deprecated and will eventually be removed.
    // @param data (Array of Record) Array of records to apply as the client-side test-data
    // @group clientData
    // @visibility external
    //<
    setTestData : function (data, invalidateCache) {
        this.setCacheData(data,invalidateCache);
    },

    //> @attr dataSource.sendParentNode (Boolean : false : IRWA)
    // Set this attribute if you need to send the dsRequest.parentNode to the server-side.
    // @visibility external
    //<
    sendParentNode : false,

    // clear deferred requests - support passing in requestTypes to cancel or "any" to clear
    // all pending requests - if no requestTypes passed, assume "any"
    clearDeferredRequests : function (requestTypes) {
        if (!this._deferredRequests) return;
        requestTypes = requestTypes || "any";
        if (!isc.isAn.Array(requestTypes)) requestTypes = [requestTypes];

        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("clearDeferredRequests: "+this._deferredRequests.length+" requests, "+
                "clearing those of type "+isc.echoAll(requestTypes), "cacheAllData");
        }

        if (requestTypes.contains("any")) delete this._deferredRequests;
        else {
            if (this._deferredRequests) {
                var requests = this._deferredRequests;
                for (var i=requests.length; i>=0; i--) {
                    var type = requests[i].operationType || "fetch";
                    if (requestTypes.contains(type)) this._deferredRequests.removeAt(i);
                }
                if (this._deferredRequests.length == 0) delete this._deferredRequests;
            }
        }
    },

    // process any deferred requests that are still outstanding
    processDeferredRequests : function () {
        if (!this._deferredRequests) return;
        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("processDeferredRequests: processing "+this._deferredRequests.length+
                " deferred requests", "cacheAllData");
        }

        var deferredRequests = this._deferredRequests;
        this.clearDeferredRequests();
        // call all deferred requests
        for (var i = 0; i < deferredRequests.length; i++) {
            this.sendDSRequest(deferredRequests[i]);
        }
    },

    //> @method dataSource.invalidateCache()
    // Invalidate the cache when +link{cacheAllData} or +link{dataSource.clientOnly,clientOnly} are true.
    // @group clientData
    // @visibility external
    //<
    invalidateCache : function () {
        if (!this.cacheAllData && !this.clientOnly) return;

        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("invalidateCache: invalidating client-side cache", "cacheAllData");
        }

        delete this.cacheData;
        delete this.testData;
        this.cacheLastFetchTime = 0;
        if (this.cacheResultSet) {
            this.cacheResultSet.destroy();
            delete this.cacheResultSet;
        }
    },

    //> @method dataSource.setClientOnly()
    // Switch into clientOnly mode, taking the cache from the cacheAllData ResultSet if it
    // exists.
    // @group clientData
    // @visibility external
    //<
    setClientOnly : function (clientOnly) {
        if (clientOnly) {
            this.clientOnly = true
            if (this.cacheAllData) {
                if (this.cacheResultSet) {
                    if (this.logIsInfoEnabled("cacheAllData")) {
                        this.logInfo("setClientOnly: sourcing from client-cache", "cacheAllData");
                    }
                    this.cacheData = this.testData = this.cacheResultSet.getAllRows();
                }
            } else {
                this.clearDeferredRequests();
                this.invalidateCache();
                this.performDSOperation("fetch");
            }
        }
    },

    //> @method dataSource.hasAllData()
    // When +link{dataSource.cacheAllData} is true, has all the data been retrieved to the client?
    // @return (Boolean) All data has been fetched from the server and is available client-side
    // @group clientData
    // @visibility external
    //<
    hasAllData : function () {
        if (this.cacheResultSet) return this.cacheResultSet.lengthIsKnown();
        else return false;
    },

    //> @attr dataSource.autoCacheAllData (Boolean : false : IRW)
    // When a DataSource is not +link{dataSource.cacheAllData}:true and a fetch results in the
    // entire dataset being retrieved, this attribute being set to true causes the DataSource
    // to automatically switch to <code>cacheAllData:true</code> and prevent further server-trips for fetch
    // requests.
    // <p>
    // +link{dataSource.cacheAllData, cacheAllData} is automatically enabled in either of these conditions:
    // <ul>
    //   <li>
    //     The request has no criteria and no startRow/endRow request properties. The latter can
    //     be accomplished by disabling paging with a +link{dataBoundComponent.dataFetchMode,dataFetchMode}
    //     setting of "basic" or "local" or by an explicit fetchData request with those request properties
    //     excluded.
    //  </li>
    //  <li>
    //     The request has no criteria but has startRow/endRow specified and the response received has
    //     all data available (<code>startRow:0</code> and <code>endRow:totalRows</code>).
    //  </li>
    // </ul>
    // @group clientData
    // @visibility external
    //<
    autoCacheAllData: false,

    //> @attr dataSource.useTestDataFetch (boolean : null : IRW)
    // When set, causes a +link{clientOnly, client-only} or +link{cacheAllData} DataSource to
    // create a second DataSource to perform it's one-time fetch.  By default, this attribute
    // will be considered true when clientOnly is true, cacheAllData is false or unset and
    // a dataURL or testFileName is specified on the DataSource.
    // @group clientData
    // @visibility external
    //<


    criteriaContainsDates : function (criteria) {
        var fieldNames = this.getCriteriaFields(criteria);

        if (fieldNames && fieldNames.length > 0) {
            for (var i=0; i<fieldNames.length; i++) {
                var field = this.getField(fieldNames[i]);
                if (!field) continue;
                if (isc.SimpleType.inheritsFrom(field.type, "date") ||
                            isc.SimpleType.inheritsFrom(field.type, "datetime")) {
                    return true;
                }
            }
        }

        return false;
    },

    //> @method dataSource.convertRelativeDates (Criteria : null : IRW)
    // Takes all relative date values found anywhere within a Criteria / AdvancedCriteria object
    // and converts them to concrete date values, returning the new criteria object.
    // @param criteria (Criteria) criteria to convert
    // @param [timezoneOffset] (String) optional timezone offset.  Defaults to the current timezone
    // @param [firstDayOfWeek] (integer) first day of the week (zero is Sunday).  Defaults to
    //                               +link{DateChooser.firstDayOfWeek}
    // @param [baseDate] (Date) base value for relative conversion - defaults to now
    // @return (Criteria) new copy of the criteria with all relative dates converted
    // @visibility external
    //<
    convertRelativeDates : function (criteria, timezoneOffset, firstDayOfWeek, baseDate) {
        // just bail if passed null criteria
        if (!criteria) return null;

        if (!this.criteriaContainsDates(criteria)) {
            // there are no date fields in the criteria - no point continuing, just return it
            return criteria;
        }

        if (!this.isAdvancedCriteria(criteria) && criteria.operator == null) {
            // this is neither an AdvancedCriteria nor a simple Criterion object so no point
            // parsing it, just return it as-is
            return criteria;
        }

        // get a copy of the criteria to alter and return - it's ok to use clone() here as
        // we've already confirmed the param is criteria above
        var result = isc.clone(criteria);

        baseDate = baseDate || new Date();

        if (firstDayOfWeek == null) firstDayOfWeek = isc.DateChooser ? isc.DateChooser.firstDayOfWeek : 0;

        if (result.criteria && isc.isAn.Array(result.criteria)) {
            // complex sub-criteria, call this method again with that criteria
            var subCriteria = result.criteria;

            for (var i = subCriteria.length-1; i>=0; i--) {
                var subItem = subCriteria[i];

                if (!subItem) {
                    if (this.logIsInfoEnabled("relativeDates")) {
                        this.logInfo("Removing NULL subcriteria...", "relativeDates");
                    }
                    result.criteria.removeAt(i);
                } else {
                    if (subItem.criteria && isc.isAn.Array(subItem.criteria)) {
                        if (this.logIsInfoEnabled("relativeDates")) {
                            this.logInfo("Calling convertRelativeDates from convertRelativeDates "+
                                "- data is:\n\n"+isc.echoFull(subItem)+"\n\n"+
                                "criteria is: \n\n"+isc.echoFull(criteria)
                                ,"relativeDates"
                            );
                        }

                        result.criteria[i] = this.convertRelativeDates(subItem, timezoneOffset,
                            firstDayOfWeek, baseDate);

                        if (this.logIsInfoEnabled("relativeDates")) {
                            this.logInfo("Called convertRelativeDates from convertRelativeDates "+
                            "- data is\n\n" + isc.echoFull(result.criteria[i]), "relativeDates");
                        }
                    } else {
                        result.criteria[i] = this.mapRelativeDate(subItem, baseDate);
                    }
                }
            }
        } else {
            // simple criterion
            result = this.mapRelativeDate(result, baseDate);
        }

        if (this.logIsInfoEnabled("relativeDates")) {
            this.logInfo("Returning from convertRelativeDates - result is:\n\n"+
                isc.echoFull(result)+"\n\n"+
                "original criteria is: \n\n"+isc.echoFull(criteria)
                ,"relativeDates"
            );
        }

        return result;
    },

// helper method to map the relative date in a single criterion
    mapRelativeDate : function (criterion, baseDate) {
        var result = isc.addProperties({}, criterion),
            rangeStart,
            value
        ;

        baseDate = baseDate || new Date();

        var fieldName = result.fieldName,
            field = fieldName ? this.getField(fieldName) : null,
            type = field ? field.type : null
        ;

        var logicalDate = isc.SimpleType.inheritsFrom(type, "date") &&
                            !isc.SimpleType.inheritsFrom(type, "datetime");

        if (result.value && isc.isAn.Object(result.value) && result.value._constructor == "RelativeDate")
        {
            // we have a criterion with a "value" and it's a relativeDate - parse it now
            value = result.value.value;

            var tempValue = isc.DateUtil.getAbsoluteDate(value, baseDate,
                                            result.value.rangePosition, logicalDate || result.value.logicalDate);

            // if the field is a "datetime", we may want to manipulate the criteria a bit,
            // without losing the relativeDate original, and depending on the operator
            // -----
            // we only want to to this for some of the pre-defined internal relativeDate
            // strings, those that represent a specific day - $yesterday, $today and $tomorrow
            if (!logicalDate && ["$yesterday", "$today", "$tomorrow"].contains(value)) {
                switch (result.operator) {
                    case "equals":
                        // equals for a relativeDate needs to do a between dayStart/dayEnd
                        result.operator = "betweenInclusive";
                        result.start = isc.DateUtil.getStartOf(tempValue, "D");
                        result.end = isc.DateUtil.getEndOf(tempValue, "D");
                        delete result.value;
                        break;
                    case "notEqual":
                        // notEqual for a relativeDate needs to do a NOT between dayStart/dayEnd
                        result._constructor = "AdvancedCriteria";
                        result.operator = "not";
                        result.criteria = [
                            { operator: "betweenInclusive", fieldName: result.fieldName,
                                start: isc.DateUtil.getStartOf(tempValue, "D"),
                                end: isc.DateUtil.getEndOf(tempValue, "D")
                            }
                        ];
                        delete result.value;
                        delete result.fieldName;
                        break;
                    case "lessThan":
                    case "greaterOrEqual":
                        // less/greaterOrEqual for a relativeDate needs to do a dayStart
                        result.value = isc.DateUtil.getStartOf(tempValue, "D");
                        break;
                    case "greaterThan":
                    case "lessOrEqual":
                        // startsWith for a relativeDate needs to do a dayEnd
                        result.value = isc.DateUtil.getEndOf(tempValue, "D");
                        break;
                    default:
                        result.value = tempValue;
                }
            } else {
                result.value = tempValue;
            }
        } else {
            if (result.start && isc.isAn.Object(result.start) &&
                result.start._constructor == "RelativeDate")
            {
                // we have a criterion with a "start" and it's a relativeDate - parse it now
                // Always default to start of range for ambiguous shortcuts like "$today"
                value = result.start.value;
                result.start = rangeStart = isc.DateUtil.getAbsoluteDate(value, baseDate,
                                                    "start", logicalDate || result.start.logicalDate);
            }
            if (result.end && isc.isAn.Object(result.end) &&
                result.end._constructor == "RelativeDate")
            {
                // we have a criterion with an "end" and it's a relativeDate - convert it now
                // Default to end of ambiguous shortcuts like "$today"
                value = result.end.value;
                result.end = isc.DateUtil.getAbsoluteDate(value, baseDate, "end", logicalDate || result.end.logicalDate);
            }
        }

        return result;
    },

    //> @attr dataSource.autoConvertRelativeDates (Boolean : true : IR)
    // Whether to convert relative date values to concrete date values before sending to the
    // server.  Default value is true, which means that the server does not need to understand
    // how to filter using relative dates - it receives all date values as absolute dates.
    //
    // @serverDS allowed
    // @visibility external
    //<

    autoConvertRelativeDates: true,

    //> @attr dataSource.showFieldsAsTree (boolean : false : IR)
    // If set, certain components that show lists of fields from this DataSource (including the
    // +link{FieldPicker}) will show them as a tree.
    // <P>
    // The tree structure is created by using +link{tree.modelType,modelType:"parent"} with the
    // +link{dataSourceField.fieldTreeId} and +link{dataSourceField.fieldTreeParentId} properties.
    //<
    showFieldsAsTree: false,

    //> @attr dataSource.fieldTreeRootValue (boolean : null : IR)
    // Value of +link{tree.rootValue}} used if +link{dataSource.showFieldsAsTree} is true
    //<


    //> @attr dataSource.useAnsiJoins (boolean : null : [IR])
    // For DataSources using the +link{group:sqlDataSource,SmartClient SQL engine} for
    // persistence, whether to use ANSI-style joins (ie, joins implemented with JOIN directives
    // in the table clause, as opposed to additional join expressions in the where clause).
    // The default value of null has the same meaning as setting this flag to false.
    // <P>
    // Note, outer joins (see +link{dataSourceField.joinType,joinType}) only work with certain
    // database products if you choose not to use ANSI joins.  Other than that, the join
    // strategies are equivalent.
    // <P>
    // If you wish to switch on ANSI-style joins for every DataSource, without the need to
    // manually set this property on all of them, set
    // +link{group:server_properties,server.properties} flag
    // <code>sql.useAnsiJoins</code> to true.
    //
    // @group serverDataIntegration
    // @requiresModules SCServer
    // @serverDS only
    // @visibility external
    //<


    //> @attr dataSource.trimMilliseconds (boolean : null : IR)
    // For this dataSource, should the millisecond portion of time and datetime values be
    // trimmed off before before being sent from client to server or vice versa.  By default,
    // millisecond information is preserved (ie, it is not trimmed).  You only need to consider
    // trimming millisecond values if their presence causes a problem - for example, a custom
    // server that is not expecting that extra information and so fails parsing.
    // <p>
    // Note that there is no inherent support for millisecond precision in SmartClient widgets;
    // if you need millisecond-precise visibility and editability of values in your client,
    // you must write custom formatters and editors (or sponsor the addition of such things to
    // the framework).  Server-side, millisecond-precise values are delivered to and obtained
    // from DataSourcea, so DataSource implementations that are capable of persisting and
    // reading millisecond values should work transparently.  Of the built-in DataSource types,
    // the JPA and Hibernate DataSources will transparently handle millisecond-precise values
    // as long as the underlying database supports millisecond precision, and the underlying
    // column is of an appropriate type.
    // <p>
    // The SQLDataSource was designed for accuracy to the nearest second, and making it support
    // millisecond accuracy requires a couple of steps:<ul>
    // <li>Use a column type that supports millisecond accuracy.  For example, Oracle and
    // PostgreSQL have a "timestamp(3)" type, MySQL version 5.6.4 and greater has "datetime(3)",
    // and SQL Server 2008 and greater has "datetime2"</li>
    // <li>Create your tables manually; if you allow SmartClient to do it by importing through
    // the Admin Console, it will normally choose a column type that does not support
    // sub-second precision</li>
    // <li>Use +link{dataSourceField.customUpdateExpression} in conjunction with the utility
    // method <code>DataTools.formatDate()</code> to provide a correctly-formatted date value
    // for INSERT and UPDATE operations.  Because an instance of DataTools is provided in the
    // Velocity context via the <code>$util</code> variable, you can write
    // customUpdateExpressions like this (consult the vendor's documentation for the correct
    // format to use for your particular database):<pre>
    //    $util.formatDate("yyyy-MM-dd hh:mm:ss.SSS", $values.myDateField)
    // </pre></li>
    // </ul>
    // @serverDS allowed
    // @visibility external
    //<

    // Used by getFilenameField(), getFilesizeField() and getDateCreatedField()
    _filenameField: {},
    _filesizeField: {},
    _dateCreatedField: {}



});

//> @type ValueMap
// A ValueMap defines the set of legal values for a field, and optionally allows you to provide
// a mapping from stored values to values as seen by the end user.
// <P>
// A valueMap can be specified as either an Array of legal values, or as an +link{type:Object}
// where each property maps a stored value to a user-displayable value.  See
// +link{DataSourceField.valueMap} for how to express a ValueMap in
// +link{group:componentXML,Component XML}.
// <P>
// A ValueMap can be entirely static or entirely dynamic, with many options in between.  For
// example, a ValueMap may be:
// <ul>
// <li> statically defined in a JavaScript or XML file.  Such a valueMap changes only when
// application code is upgraded.
// <li> generated dynamically by server code when the application first loads, for example,
// by generating JavaScript or XML dynamically in a .jsp or .asp file.  Such a valueMap may be
// different for each session and for each user.
// <li> loaded on demand from a DataSource, via the
// +link{PickList.optionDataSource,optionDataSource} property, or via a call to
// +link{dataSource.fetchData()} where a valueMap is derived dynamically from the returned data
// (see +link{dataSource.fetchData()} for an example).  Such a valueMap may be updated at any
// time, for example, every time the user changes a related field while editing data.
// </ul>
// See also the +link{group:smartArchitecture,SmartClient Architecture Overview} to understand
// the best architecture from a performance and caching perspective.
// @serverDS allowed
// @visibility external
//<

//> @class DataSourceField
// Metadata about a DataSourceField, including its type and validators.
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<

//> @groupDef basics
// Basic definition of a DataSource field
// @visibility external
//<

//> @attr dataSourceField.name                 (identifier : null : [IR])
// Name for this field.
// <p>
// The field name is also the property in each DataSource record which holds the value for this
// field.
// <P>
// Must be unique across all fields within the DataSource as well as a valid JavaScript identifier,
// as specified by ECMA-262 Section 7.6.
// <P>
// <B>NOTE:</B> The <smartclient>+link{String.isValidID()}</smartclient><smartgwt>StringUtil.isValidID()</smartgwt>
// function can be used to test whether a name is a valid JavaScript identifier.
//
// @group basics
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.type                 (FieldType : null : [IR])
// Type of this field.  Required for all DataSource fields.
// <P>
// Field type may imply automatic validators (for example, an integer field cannot accept the
// value "foo").  Field type also affects the default behaviors of DataBound components, for
// example, if a field is declared as type "date", components that edit that field will
// automatically choose a date-editing interface with pop-up date picker.
// @group basics
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.mimeType (String : null : IR)
// For a +link{group:binaryFields,binary field}, sets a fixed mime type for all files stored
// to this field.  Should be set to a standard mime type string, for example, "text/plain" for
// a .txt file.
// <P>
// This is useful if a binary field only stored one type of file and you chose not to store
// filenames, since the extension of the file name is normally used to determine the mime type.
// @serverDS only
// @visibility external
//<

// Validation
// --------------------------------------------------------------------------------------------

//> @attr dataSourceField.required             (boolean : null : [IR])
// Indicates this field must be non-null in order for a record to pass validation.
// <P>
// Note that <code>required</code> should not be set for a server-generated field, such as a
// sequence, or validation will fail on the client.
// @group dataType
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.autoGenerated        (boolean : null : [IR])
// Indicates that values for this field are automatically generated by the database or ORM
// provider.  Setting this flag makes the field behave somewhat like a
// +link{sequenceName,sequence field}, in that server-side logic does not expect client code
// to provide values for the field on "add" operations.  However, it is not entirely the same
// as a sequence:<ul>
// <li>Sequences must be integers, whereas autoGenerated fields can be of any type</li>
// <li>SmartClient Server's DataSource implementations are capable of discovering the value
//     that was generated by the database for sequence fields.  With autoGenerated fields,
//     your code is responsible for doing this - similar to sequence fields when the
//     +link{type:SequenceMode,sequenceMode} is  "none"</li>
// </ul>
// @group dataType
// @see sequenceName
// @see type:FieldType
// @see type:SequenceMode
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.nillable             (boolean : null : IR)
// Controls whether an explicit null-valued Record attribute for this field
// should result in <code>xsi:nil</code> being used to transmit the value when serializing
// to XML, like so:
// <pre>
// &lt;book&gt;
//     &lt;title&gt;Beowulf&lt;/title&gt;
//     &lt;author xsi:nil="true"/&gt;
// &lt;/book&gt;
// </pre>
// If <code>nillable</code> is not set, no XML element will be generated for
// the explicit null value.
// @group dataType
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.valueMap             (ValueMap : null : [IR])
// A +link{type:ValueMap} is a set of legal values for a field.
// <p>
// The valueMap can be specified as either an Array of legal values, or as an
// +link{type:Object} where each property maps a stored value to a user-displayable value.
// <p>
// To enforce that a field should be constrained to only the values in the valueMap, either
// declare +link{dataSourceField.type,field.type} as "enum", or use a
// +link{ValidatorType} of "isOneOf" with explicitly listed values.  Otherwise,
// although a normal +link{SelectItem} control will only allow values from the valueMap to be
// entered, other controls such as a +link{ComboBoxItem,ComboBox} will allow other values
// to be entered.
// <p>
// In XML, a valueMap that specifies only a list of legal values is specified as follows:
// <pre>
//   &lt;valueMap&gt;
//       &lt;value&gt;Pens &amp; Pencils&lt;/value&gt;
//       &lt;value&gt;Stationery&lt;/value&gt;
//       &lt;value&gt;Computer Products&lt;/value&gt;
//       &lt;value&gt;Furniture&lt;/value&gt;
//       &lt;value&gt;Misc&lt;/value&gt;
//   &lt;/valueMap&gt;
// </pre>
// A ValueMap that specifies stored values mapped to user-visible values is specified as
// follows:
// <pre>
//   &lt;valueMap&gt;
//       &lt;value ID="1"&gt;Pens &amp; Pencils&lt;/value&gt;
//       &lt;value ID="2"&gt;Stationery&lt;/value&gt;
//       &lt;value ID="3"&gt;Computer Products&lt;/value&gt;
//       &lt;value ID="4"&gt;Furniture&lt;/value&gt;
//       &lt;value ID="5"&gt;Misc&lt;/value&gt;
//   &lt;/valueMap&gt;
// </pre>
//
// @group dataType
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.valueMapEnum (String : null : IR)
// Fully qualified Java className of an Enum that should be used to derive this field's
// +link{valueMap}.
// <P>
// This has the same behavior of auto-deriving a <code>valueMap</code> from a Java Enum as
// +link{DataSource.autoDeriveSchema} except it applies only to one field.
// <P>
// If you enable <code>autoDeriveSchema</code> there is no need to set
// <code>valueMapEnum</code> for Enum fields unless you want to actually <i>override</i> the
// behavior to use a different Enum for the field (eg, a superclass Enum that is more
// restrictive).
//
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.validators           (Array of Validator : null : [IR])
// Validators to be applied to this field.
// <p>
// Validators are applied whenever there is an attempt to save changes to a field.
// <p>
// For the available set of built-in validators, and how to define a custom validator, see the
// +link{class:Validator} class.
//
// @see class:Validator
// @group dataType
// @group validation
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.length               (number : null : [IR])
// Maximum number of characters allowed.  Applicable only to fields of text type.
// <P>
// <b>NOTE:</b> For DataSources of type "sql", this property has a bearing on the type of
// column we use when the underlying table is created by a DataSource
// +link{group:sqlDataSource,import} in the +link{group:adminConsole,Admin Console}.  Below
// a certain length (which is database-specific, see below), we use standard <code>VARCHAR</code>
// columns; above that length, we use an alternate strategy (again, database-specific).  For
// these long fields, we sometimes also generate different SQL for "update" and "add"
// operations, using JDBC "?" replacement parameters rather than embedding values directly in
// the generated SQL; whether or not this is done depends entirely on what the underlying
// database product and/or JDBC driver will allow.
// <P><br>
// <b>Table of field length limits for supported databases:</b><p>
// <table style="font-size:10px;text-align:center;border:1px solid black;">
// <tr><td style="color:white;background-color:black;"><b>Database product</b></td>
//     <td style="color:white;background-color:black;"><b>VARCHAR limit *</b></td>
//     <td style="color:white;background-color:black;"><b>Type used above limit</b></td></tr>
// <tr><td>HSQLDB</td><td>None</td><td>-</td></tr>
// <tr><td>IBM DB2</td><td>4000</td><td>CLOB</td></tr>
// <tr><td>Firebird</td><td>32767</td><td>BLOB with subtype 1</td></tr>
// <tr><td>Informix</td><td>255 / 32739</td><td>LVARCHAR / TEXT ** </td></tr>
// <tr><td> Microsoft SQL Server </td><td>8000</td><td>TEXT</td></tr>
// <tr><td>MySQL</td><td> 255 / 65535 / 16M </td><td> TEXT / MEDIUMTEXT / LONGTEXT *** </td></tr>
// <tr><td>Oracle</td><td>4000</td><td>CLOB</td></tr>
// <tr><td>PostgreSQL</td><td>4000</td><td>TEXT</td></tr>
// </table><br>
// <b>*</b> The "VARCHAR limit" referred to here is a limit used by the SmartClient Server; it
// is not necessarily imposed by the database.  For example, DB2's VARCHAR limit is not 4000
// characters; it actually varies from about 4K to about 32K, depending on how the server has
// been configured.<p>
// <b>**</b> Informix has a limit of just 255 characters for VARCHAR, but has a native LVARCHAR
// type which supports nearly 32K characters without needing to fall back on long datatypes.
// Therefore, with that one product, we have two thresholds for a change in storage type.<p>
// <b>***</b> MySQL has a limit of 255 characters for VARCHAR, 65,535 characters for TEXT and
// 16,777,215 for MEDIUMTEXT; therefore, with that one product, we have three thresholds for a
// change in storage type.
// @group dataType
// @serverDS allowed
// @visibility external
// @example longText
//<

// XML
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.valueXPath      (XPathExpression : null : [IR])
// XPath expression used to retrieve the field's value.
// <P>
// This XPath expression will be evaluated in the scope of the record objects selected by
// the +link{dataSource.recordXPath}.  For XML data
// (+link{DataSource.dataFormat,dataFormat:"xml"}) this means a call to
// +link{XMLTools.selectString()} passing the selected XML element.  For JSON data
// (+link{DataSource.dataFormat,dataFormat:"json"}), this means a call to
// +link{XMLTools.selectObjects()} passing the selected JSON object.
// <P>
// In the absence of a <code>valueXPath</code>, for JSON data the value for the field will
// be the value of the same-named property in the record object selected by
// +link{DataSource.recordXPath,recordXPath}.
// <P>
// For XML data, the value will be the attribute or subelement named after the field name.
// For example, for a field "author" on a record element &lt;book&gt;, the following structures
// require no valueXPath:
// <pre>
//    &lt;book author="Mark Jones"/&gt;
//
//    &lt;book&gt;
//        &lt;author&gt;Mark Jones&lt;/author&gt;
//    &lt;/book&gt;
// </pre>
// <P>
// If <code>valueXPath</code> is not required for your field because of the default handling
// described above, don't specify it, as it's slightly slower.
// <p>
// To learn about XPath, try the following search:
// <a href="http://www.google.com/search?q=xpath+tutorial" target="_blank"
// >http://www.google.com/search?q=xpath+tutorial</a>
// <P>
// <B>Using valueXPath with the SmartClient server</B>
// <p>
// If you're using the SmartClient server to return data via the DSResponse object (or
// indirectly doing so using DataSource DMI), the valueXPath you specify on the DataSource
// fields will be applied to the data you return via the
// +externalLink{http://commons.apache.org/jxpath/,JXPath} library.
// <P>
// If you are returning Java Beans as your DSResponse data, normally each dataSource field
// receives the value of the same-named Java Bean property, that is, a field "zipCode" is
// populated by looking for "getZipCode()" on the objects passed as DSResponse data.  You can
// use <code>valueXPath</code> to retrieve properties from subobjects, so long as a chain of
// getter methods exists that corresponds to the valueXPath.  For example, a valueXPath of
// "address/zipCode" expects to call "getAddress()" on the bean(s) passed to
// DSResponse.setData(), followed by "getZipCode()" on whatever object "getAddress()" returns.
// <P>
// When you are saving data, the inbound DSRequest values, available as a Java Map, will use
// just dataSource field names as Map keys, not the valueXPath used to derive them.  However,
// to achieve bidirectional valueXPath binding, you can use the server-side method
// dataSource.setProperties() to use the valueXPath when setting properties on your server
// object model.  When applied as a setter, an XPath like "address/zipCode" attempts
// "getAddress()" followed by "setZipCode()" on the returned object.  JXPath also has some
// ability to auto-create intervening objects if they are missing, such as auto-creating an
// "address" subobject when applying "address/zipCode" as a valueXPath.
// <P>
// See the
// +externalLink{http://jakarta.apache.org/commons/jxpath/,JXPath} library documentation for
// complete details, including other types of server object models supported, such as
// server-side XML.
//
// @see DataSource.setProperties() (Java API)
// @see DSResponse.setData() (Java API)
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
// @example xpathBinding
//<

//> @attr dataSourceField.valueWriteXPath      (XPathExpression : null : [IR])
// Alternative XPath expression used to set the field's value.
// <P>
// If is not set, then +link{DataSourceField.valueXPath,dataSourceField.valueXPath} is
// used, see its description for details.
//
// @see DataSource.setProperties() (Java API)
// @see DSResponse.setData() (Java API)
// @group clientDataIntegration
// @serverDS only
// @visibility xmlBinding
//<

//> @method dataSourceField.getFieldValue() [A]
// Function or JavaScript expression used to retrieve the field's value from the XML element or
// JSON record returned from a web service.
// <P>
// This is an advanced API for use when a +link{dataSourceField.valueXPath,valueXPath} setting
// is insufficient to derive a field's value, yet an implementation of
// +link{DataSource.transformResponse()} is overkill.
//
// @param record (Object or XMLElement) record object selected from web service response data
//                                      by +link{operationBinding.recordXPath,recordXPath}
// @param value (any) default value derived by the method described in
//                    +link{dataSourceField.valueXPath}
// @param field (DataSourceField) DataSourceField definition
// @param fieldName (String) name of the DataSource field
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr dataSourceField.lenientXPath (boolean : null : IR)
// Indicates that getting +link{dataSourceField.valueXPath} for this field should
// not perform any validation at all and will return null for non existing XPaths.
// Otherwise warning message will be logged for non-existing XPath or with null objects
// in the middle of XPath.
// <P>
// NOTE: this applies to server-side processing of valueXPath only.
//
// @serverDS allowed
// @visibility external
// @deprecated No longer needs to be set since the framework now defaults to suppressing errors
// for null values in the middle of Xpath, and only invalid XPath will cause warning be logged.
//<

// Component Binding
// --------------------------------------------------------------------------------------------

//> @groupDef componentBinding
// Properties that control how a DataBound Component binds to this DataSource
// @title Component Binding
// @visibility external
//<

//> @attr dataSourceField.title                (String : null : [IR])
// Default user-visible title for this field.
// <p>
// This will be picked up by DataBound components and other views over this DataSource.
// <p>
// Note this property frequently does not need to be set since +link{DataSource.autoDeriveTitles} (on by
// default) usually picks an appropriate user-visible title if you have a reasonable naming convention
// for your field names.
// <P>
// Note that if this field is being displayed in a +link{ListGrid} bound to this dataSource,
// the +link{listGridField.headerTitle} attribute may be used to specify a
// different string for display in the listGrid column header.
//
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.escapeHTML    (Boolean : null : [IR])
// When data values are displayed in DataBound components, by default strings will be interpreted
// as HTML by the browser in most cases.
// <P>
// If set, this property will be picked up by components bound to this dataSource, notifying them
// that any HTML characters should be escaped when displaying values for this field.
// @see ListGridField.escapeHTML
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.canView              (boolean : null : [IR])
// If false, this property indicates that this field is considered "server only".  This means:
// <ul>
// <li>Components cannot bind to the field; even if you explicitly add a field with the same
//     name to your +link{DataBoundComponent,dataBoundComponent}, it will be dropped</li>
// <li>If you are using SmartClient Server, the client will never be sent a value for the
//     field</li>
// <li>If you are using SmartClient Server, then similar to +link{canEdit,canEdit}, no updates
//     to the field are allowed from the client.  If you explicitly add a value for the field
//     to, eg, a record you are passing to +link{dataSource.updateData}, the server will strip
//     the value out of the record before processing the update request.
// </ul>
// <code>canView:false</code> is <b>not</b> the same thing as +link{hidden,hidden}.  Use
// <code>canView:false</code> when you want to prevent the client from ever seeing a field's
// definition or values; use <code>hidden:true</code> if it is fine from a security perspective
// that a field's definition and values are sent to the browser, but the field should not by
// default appear in user interface elements (but could do in some cases, like a special screen
// for advanced users or administrators, for example).
// <p>
// Note that this property must be set explicitly to false to have an effect; a null or
// undefined setting is treated the same as true.
// <P>
// This property is used to implement field-level view security: failing a
// +link{viewRequiresAuthentication,viewRequiresAuthentication},
// +link{viewRequiresRole,viewRequiresRole} or +link{viewRequires,viewRequires} test is
// equivalent to setting <code>canView:false</code> on the field (and, indeed, from the
// client's perspective, the field <em>has</em> had <code>canView:false</code> set).
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.canEdit              (boolean : null : [IR])
// Controls whether, by default, DataBoundComponents consider this field editable.
// Set to <code>false</code> to draw this field read-only.
// <p>
// For a field that should never be changed from the UI, consider using +link{canSave} instead,
// which will both mark a field as non-editable and reject any attempt to programmatically
// change the value on the server (when using the SmartClient Server).
// <P>
// This attribute may not effect all dataBoundComponents - the
// +link{databoundComponent.canEditFieldAttribute} may be set at the component level to look
// for a different attribute on the dataSourceField, and components allow developers to explicitly
// override this default (see +link{ListGridField.canEdit}. +link{FormItem.canEdit} for example).
//
// @group componentBinding
// @see dataSourceField.canFilter
// @see dataSourceField.canSave
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.canSave (boolean : null : [IR])
// Whether values in this field can be updated and saved to the dataSource.
// <P>
// If set to <code>false</code>, this field will default to being non-editable in standard editing
// components (+link{DynamicForm}, editable +link{ListGrid}), but will be editable when displayed
// for filtering purposes only (in a +link{SearchForm} or +link{ListGrid.showFilterEditor,ListGrid
// filter editor}.  If +link{dataSourceField.canEdit,canEdit} is explicitly specified it will take
// precedence over this client-side behavior, but the server will still enforce the no-save
// policy (described below).
// <p>
// NOTE: If you are using SmartClient Server and have specified <code>canSave: false</code>
// for a field in a DataSource definition (<code>.ds.xml</code> file), this is enforced on
// the server.  This means that we will strip out any attempt to set the value of such a
// field before trying to process any update or add request, similar to what happens when
// a +link{editRequiresAuthentication,field-level declarative security check} fails.
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.canFilter (boolean : null : IR)
// Should the user be able to filter data by this field?
// Affects whether this field will show up in dataBoundComponents with UI for filtering data.
// <P>
// @see SearchForm.showFilterFieldsOnly
// @see SearchForm.canEditFieldAttribute
//
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.editorType           (FormItem className : null : [IR])
// Sets the default FormItem to be used whenever this field is edited (whether in a grid, form,
// or other component).
// <P>
// If unset, a FormItem will be automatically chosen based on the type of the field, by the
// rules explained +link{type:FormItemType,here}.
//
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.readOnlyEditorType           (FormItem className : null : [IR])
// Sets the default FormItem to be used if this field is marked as
// +link{dataSourceField.canEdit,canEdit false} and displayed in an editor component such
// as a DynamicForm.
// <P>
// This property may also be specified at the type level by specifying
// +link{SimpleType.readOnlyEditorType}.
//
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.displayFormat        (DateDisplayFormat : null : [IR])
// The default date formatter to use for displaying this field.  Only applicable to fields of
// type "date" and "datetime".  Note that this property is honored when exporting directly to
// Excel spreadsheets (ie, when using XLS or XLSX/OOXML form, <b>not</b> CSV); "date" and
// "datetime" fields with this property set will deliver real dates and formatting information
// to Excel, rather than formatted strings or unformatted dates.
//
// @serverDS allowed
// @visibility external
// @deprecated Use +link{dataSourceField.dateFormatter} or +link{dataSourceField.timeFormatter}
//   instead.
//<

//>    @attr dataSourceField.dateFormatter (DateDisplayFormat : null : [IRWA])
// Preferred display format to use for date type values within this field.
// If this property is set on a field displayed in a databound component such as a
// +link{DynamicForm} or +link{ListGrid} it will be respected (See +link{formItem.dateFormatter} and
// +link{listGridField.dateFormatter}).
// <P>
// Note that this property is also honored when exporting directly to
// Excel spreadsheets (ie, when using XLS or XLSX/OOXML form, <b>not</b> CSV); "date" and
// "datetime" fields with this property set will deliver real dates and formatting information
// to Excel, rather than formatted strings or unformatted dates.
//
// @group appearance
// @visibility external
//<
//dateFormatter:null

//>    @attr dataSourceField.timeFormatter (TimeDisplayFormat : null : [IRWA])
// Preferred time-format to apply to date type values within this field.  If this property is
// specified on a field displayed within a dataBound component such as a +link{listGrid} or
// +link{dynamicForm}, any dates displayed in this field will be formatted as times using the
// appropriate format.
// <P>
// This is most commonly only applied to fields specified as type <code>"time"</code> though
// if no explicit +link{formItem.dateFormatter} is specified it will be respected for other
// fields as well.
// <P>
// See +link{listGridField.timeFormatter} and +link{formItem.timeFormatter} for more information.
//
// @group appearance
// @visibility external
//<
//timeFormatter:null

//> @attr dataSourceField.decimalPrecision (number : null : [IRW])
// Applies only to fields of type "float" and affects how many significant digits are shown.
// <P>
// For example, with decimalPrecision 3, if the field value is 343.672677, 343.673 is shown.
// <P>
// If the value is 125.2, 125.2 is shown - decimalPrecision will not cause extra zeros to be
// added.  Use +link{dataSourceField.decimalPad} for this.
// <P>
// A number is always shown with its original precision when edited.
//
// @group appearance
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.decimalPad (number : null : [IRW])
// Applies only to fields of type "float" and enforces a minimum number of digits shown after
// the decimal point.
// <P>
// For example, a field value of 343.1, 343.104 and 343.09872677 would all be shown as 343.10
// if decimalPad is 2.
// <P>
// The original unpadded value is always shown when the value is edited.
//
// @group appearance
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.precision (number : null : [IRW])
// Applies only to fields of type "float" or "integer" and affects how many significant
// digits are shown.
// <P>
// For example, a field value of 343.1, 343.104 and 343.09872677 would all be shown as 343.10
// if precision is 5.
// <P>
// The original value is always shown when the value is edited.
//
// @group appearance
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.hidden (boolean : false : [IR])
// Whether this field should be hidden from users by default within a DataBound component.
// This is generally used for internal IDs and other fields not meaningful to users.
// <P>
// See +link{dataSourceField.detail} for fields that should be hidden in a summary view such as
// a +link{ListGrid}, but still available to the user.
// <p>
// <b>NOTE:</b> This property is <b>not</b> a security setting - data for hidden fields is
// still delivered to the client, it just isn't shown to the user.  If you wish to make sure
// that only appropriate data reaches the client, use +link{operationBinding.outputs},
// +link{canView}:false on the field, or a field-level declarative security setting like
// +link{dataSourceField.viewRequiresRole}.
//
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.ignore (boolean : false : [IR])
// Whether this field should be completely excluded from this dataSource, as if it had never been
// defined.
// <P>
// If set to true, the field will be entirely omitted when serving a DataSource derived
// from a server-side definition (typically a .ds.xml file) to the client.
//
// @group componentBinding
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.detail (boolean : false : [IR])
// Whether this field should be considered a "detail" field by a +link{DataBoundComponent}.
// <P>
// Detail fields won't be shown by default in a DataBoundComponent where
// +link{DataBoundComponent.showDetailFields} is false.  This allows for some DataBound
// components, like a +link{ListGrid}, to show a summary view of records which displays only
// the most commonly viewed fields by default, while other DataBoundComponents, like a
// +link{DetailViewer}, show all fields by default.
// <P>
// In addition, the +link{formItem.showIf,formItem.showIf} property is supported in multiple
// components for conditional visibility - see for example +link{listGridField.showIf} and
// +link{formItem.showIf}).
//
// @group componentBinding
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.excludeFromState (Boolean : null : IR)
// If true, then this field is excluded from the bound component's view state. In addition,
// the field will not be selected as the default title field
// by +link{DataBoundComponent.getTitleField()}
// if +link{DataBoundComponent.titleField} is not provided.
// @group viewState
//<

//> @attr dataSourceField.emptyDisplayValue (HTMLString : null : [IR])
// Text to be used for display by client-side components when this field has a null or
// undefined value.  This value will be overridden by a component's emptyCellValue, if set.
//
// @group appearance
// @see formItem.emptyDisplayValue
// @see listGridField.emptyCellValue
// @see detailViewerField.emptyCellValue
// @serverDS allowed
// @visibility external
//<

// Relations
// --------------------------------------------------------------------------------------------

//> @groupDef dataSourceRelations
// Properties for declaring relations between DataSources and within DataSources, such as
// master-detail relationships (a SalesOrder contains 0 or more OrderItems) and tree
// relationships (records can be linked by a parentId field).
//
// @see DataSourceField.foreignKey
// @title Relations
// @visibility external
//<

//> @attr dataSourceField.primaryKey           (boolean : false : [IR])
// Indicates <b>either</b> that this field holds a value unique across all records in this
// DataSource, <b>or</b> that it is one of a number of fields marked as primary keys, and the
// combination of the values held in all of those fields is unique across all records in the
// DataSource.  Note that the latter usage - so-called "composite" or "multipart" keys - is
// intended for support of legacy databases only: if you are able to choose an approach,
// Isomorphic recommends the use of one <code>primaryKey</code> field per DataSource, and
// ideally this field should be of +link{dataSourceField.type,type} "sequence".
// <p>
// A DataSource that can <i>only</i> perform the "fetch" operation does not require a
// primaryKey unless it contains +link{group:binaryFields,binary fields}.  If a DataSource
// allows modification of DataSource records through add, update and remove DataSource
// operations, or it contains one or more binary fields, one or more fields must be marked as
// the primary key.
// <P>
// SmartClient requires a primary key value to uniquely identify records when communicating
// updates or deletions to the server.  There is no requirement that the primaryKey field be
// mapped to an actual "primary key" in your object model, web service, or database (though
// this is the most obvious and natural thing to do, of course).  The only requirement is
// that the combined values of the primaryKey fields be unique for a given browser instance
// for the lifetime of the page.
// <P>
// If using SmartClient's +link{group:sqlDataSource,SQL engine} and generating SQL tables using
// the +link{group:adminConsole,Admin Console}, the table column generated from a primaryKey
// field will have a unique constraint applied in the database table and, if the field is of
// type "sequence", the database column will also be created as an "identity column" in those
// databases that implement sequence-type handling with identity columns.
//
// @group dataSourceRelations
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.foreignKey           (String : false : [IR])
// Declares that this field holds values that can be matched to values from another DataSource
// field, to create a relationship between records from different DataSources or even records
// within the same DataSource.
// <p>
// The format of <code>foreignKey</code> is
// <code><i>dataSourceId</i>.<i>fieldName</i></code>.
// <p>
// For a foreignKey within the same dataSource, you can omit the <code>dataSourceId</code>
// and just specify <code><i>fieldName</i></code>.  For example, to create a tree relationship
// within a DataSource:
// <pre>
//   isc.DataSource.create({
//     ID:"supplyItem",
//     fields : [
//       {name:"itemId", type:"sequence", primaryKey:true},
//       {name:"parentId", type:"integer", foreignKey:"itemId"},
//       ...
//     ]
//   });
// </pre>
// <P>
// <code>foreignKey</code> declarations also allow other automatic behaviors by
// +link{DataBoundComponent,DataBoundComponents}, such as +link{listGrid.fetchRelatedData()}.
// <p>
// For SQLDataSources foreign keys can be automatically discovered from SQL tables if
// +link{dataSource.autoDeriveSchema,autoDeriveSchema} is set.
//
// @see dataSourceField.joinType
// @group dataSourceRelations
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.childrenProperty (Boolean : false : [IR])
// If true, this property indicates that this field will hold an explicit array of child nodes
// for the current node.
// This has the same effect as specifying +link{dataSource.childrenField} to this field's
// name.
// @see dataSource.childrenField
// @group dataSourceRelations
// @serverDS allowed
// @visibility external
// @example treeLoadXML
//<


//> @attr dataSourceField.rootValue            (any : null : [IR])
// For a field that is a foreignKey establishing a tree relationship, what value indicates a
// root-level node.  Defaults to null.
// <P>
// Note that the rootValue may be overridden on a specific ResultTree instance by setting
// +link{ResultTree.rootNode}, or if the ResultTree is auto-generated by a +link{TreeGrid},
// by setting +link{TreeGrid.treeRootValue}. This allows a component to navigate a subtree of
// the hierarchical data from this dataSource starting at a particular node.
// @group dataSourceRelations
// @serverDS allowed
// @visibility external
// @example treeLoadXML
//<

//> @attr dataSourceField.sequenceName (String : null : IR)
// For a DataSource with +link{dataSource.serverType,serverType:"sql"} with a field of type
// "sequence", the name of the SQL sequence that should be used when inserting new records into
// this table.
// <P>
// Note that this is never required for SQL tables that are generated from SmartClient
// DataSources (a default sequence name of tableName + "_" + columnName is chosen, but see the
// notes below regarding this), and is never required for databases where inserting null into a
// sequence column is sufficient (MySQL, SQL Server, DB2 and others).
// <P>
// You would only need to set sequenceName if you are integrating with a pre-existing table
// stored in a database where the sequence must be named for insertion to work (Oracle,
// Postgres, Firebird) OR you are trying to use the same sequence across multiple DataSources.
// <P>
// <b>Note:</b> If you specify the <code>sql.{database type}.sequence.name.prefix</code> and/or
// <code>sql.{database type}.sequence.name.suffix</code> properties in your
// +link{group:server_properties,server.properties} file,the generated sequence name will include the prefix and/or
// suffix.  For example, with a prefix of "order_system_" and a suffix of "_seq", the sequence
// generated for column "orderNumber" on table "orders" would be:<pre>
//     order_system_orders_orderNumber_seq
// </pre>
//
// @group sqlDataSource
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.implicitSequence (Boolean : null : IR)
// For a field of +link{type:FieldType,type} "sequence" on a DataSource with
// +link{dataSource.serverType,serverType:"sql"}, this flag indicates that the field is
// implicitly bound to a sequence.  This setting means that SmartClient does not expect to
// find a value for the field in "add" operations, even if it is marked as a
// +link{primaryKey,primaryKey}, as such fields usually are.  It also means that SmartClient
// does not attempt to retrieve the field value from an actual database sequence, instead
// relying on the JDBC driver to return the generated value (see the note on
// <code>sequenceMode</code> below).
// <P>
// Implicitly bound columns are a syntactic convenience provided by some databases to simulate
// the "auto-increment" or "identity" columns available natively in other products, without
// the underlying sequence having to be explicitly referenced in SQL queries.  Currently, these
// database products support this idea:<ul>
// <li>PostgreSQL has had a "serial" column type for a long time -
// <a href=http://www.postgresql.org/docs/9.3/static/datatype-numeric.html#DATATYPE-SERIAL>
// http://www.postgresql.org/docs/9.3/static/datatype-numeric.html#DATATYPE-SERIAL</a></li>
// <li>DB2 has long supported a "GENERATED AS IDENTITY" notation for numeric fields.  This may
// or may not be implemented with a sequence - the documentation does not specify - but we
// support it via the implicitSequence mechanism because it is so similar to the implicit
// sequence approach in Oracle.
// <a href="http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=%2Fcom.ibm.db2z10.doc.apsg%2Fsrc%2Ftpc%2Fdb2z_identitycols.htm">
// http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=%2Fcom.ibm.db2z10.doc.apsg%2Fsrc%2Ftpc%2Fdb2z_identitycols.htm</a></li>
// <li>Oracle introduced a "GENERATED AS IDENTITY" notation for numeric fields in version 12c -
// <a href=http://www.oracle-base.com/articles/12c/identity-columns-in-oracle-12cr1.php>
// http://www.oracle-base.com/articles/12c/identity-columns-in-oracle-12cr1.php</a></li>
// </ul>
// If you have columns like these in your database, declare them as type "sequence" and mark
// them with the <code>implicitSequence</code> flag.
// <P>
// <b>NOTE:</b> If you use this property, you should also set the
// +link{dataSource.sequenceMode,DataSource sequenceMode} to "jdbcDriver".  This is necessary
// because SmartClient cannot directly query the implicit sequence that is being used behind the
// scenes, so we must rely on the JDBC driver to do that.
// <p>
// If you use +link{dataSource.autoDeriveSchema,autoDeriveSchema} to automatically derive a
// dataSource from table metadata, SmartClient attempts to identify these special Oracle and
// Postgres columns by heuristic examination of the metadata.  When it identifies such a
// column, it marks the corresponding dataSourceField <code>implicitSequence: true</code>,
// and changes the sequenceMode on the DataSource to "jdbcDriver".  If your table contains
// one of these columns and SmartClient does not automatically identify it, bear in mind that
// you can always set this flag manually, even if you are using <code>autoDeriveSchema</code>.
// <P>
// This setting has no effect for non-SQL dataSources, or for databases other than those
// mentioned above.
//
// @group sqlDataSource
// @see dataSource.sequenceMode
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.includeFrom (String : null : IR)
// Indicates this field should be fetched from another, related DataSource.
// <P>
// The <code>incluedFrom</code> attribute should be of the form
// "<i>dataSourceId</i>.<i>fieldName</i>", for example:
// <pre>
//    &lt;field includeFrom="supplyItem.itemName"/&gt;
// </pre>
// <P>
// A +link{foreignKey} declaration must exist between the two DataSources, establishing either
// a 1-to-1 relationship or a many-to-1 relationship from this DataSource to the related
// DataSource.  The inclusion can be indirect (traverse multiple DataSources) so long as there
// is a chain of <code>foreignKey</code> declarations from the target DataSource to the
// DataSource where the <code>includeFrom</code> field is declared.  For including from a
// related DataSource where there are multiple related records, see
// +link{dataSourceField.includeSummaryFunction, includeSummaryFunction}.
// <P>
// +link{DataSourceField.name} will default to the name of the included field, or you can
// specify a different name.
// <p>
// If both DataSources are SQLDataSources, HibernateDataSources or JPADataSources (with
// Hibernate as the provider) the related data will be retrieved via a SQL join and criteria
// and sort directions applied to the field work normally (they become part of the generated
// SQL query).
// <P>
// Otherwise, the related data will be retrieved via performing a DSRequest against
// the related DataSource once the data from the primary DataSource has been retrieved.  In
// this case, criteria or sorting directions applied to the included field are only allowed if
// data paging is not in use (for example +link{listGrid.dataFetchMode}:"basic"); otherwise,
// criteria and sort direction are ignored for the included field and a warning is logged on
// the server.
// <P>
// <b>Editing included fields</b>
// <p>
// An included field is +link{dataSourceField.canEdit,canEdit:false} by default.  Note that
// included fields are not updatable, even if you set canEdit:true; the server will simply drop
// values for included fields if client code sends them.
// <p>
// When thinking about editing an included field value, typically what is really intended is to
// edit the value of the <code>foreignKey</code> field.  For example, take the scenario of a
// system that tracks accounts and the employees assigned to manage them.  Given a DataSource
// "account" related one-to-one with DataSource "employee" by a "managerId" foreignKey field,
// we might declare an <code>includeFrom</code> so that the name of the account manager can
// be shown with each "account" record.
// <p>
// Editing the manager's name while viewing the account would be intended to pick a new account
// manager, and <b>not</b> to change the legal name of the employee who happens to be the
// current account manager.
// <p>
// To correctly set up this scenario, declare an <code>includeFrom</code> field that is hidden,
// but is used as the +link{dataSourceField.displayField,displayField} for the foreign key
// field:
// <pre>
// &lt;field name="managerId" foreignKey="employee.id" displayField="managerName"/&gt;
// &lt;field name="managerName" includeFrom="employee.name" hidden="true"/&gt;
// </pre>
// Now:
// <ul>
// <li> the "managerId" foreignKey field is shown in grids and forms, but takes its displayed
// value from the hidden <code>includeFrom</code> field
// <li> the automatically chosen editor will be a SelectItem with
// +link{SelectItem.optionDataSource,optionDataSource} set to "employees": it will allow
// picking a different "employee" record from the "employee" DataSource
// <li> saving will save the ID of a new "employee" record to the "managerId" foreign key
// field, as intended
// </ul>
// You can alternatively set <code>editorType="ComboBoxItem"</code> on the
// "managerId" field to allow typeahead search of the "employee" DataSource.
//
// @example sqlIncludeFrom
// @serverDS only
// @group dataSourceRelations
// @visibility crossDS
//<


//> @attr dataSourceField.includeVia (String : null : IR)
// For a field that uses +link{dataSourceField.includeFrom}, specifies which
// +link{dataSourceField.foreignKey,foreignKey} field should be used to find records in the
// related DataSource.
// <p>
// <code>includeVia</code> only needs to be set when you have more than one
// <code>foreignKey</code> to the <i>same</i> related DataSource.  If you have multiple
// foreignKeys to multiple <i>different</i> DataSources, there is no need to set
// <code>includeVia</code>.
// <p>
// For example, perhaps you have a DataSource "moneyTransfer" where each record represents a
// money transfer, where the source and payment currencies are different, and the list of
// currencies is stored in a related DataSource "currency".  Each "moneyTransfer" record is
// linked to <b>2</b> "currency" records, through two different foreignKey fields,
// "sourceCurrencyId" and "paymentCurrencyId".
// <p>
// The following declarations would be required to use <code>includeFrom</code> to get a
// include the field "currencySymbol" from each of the two related "currency" records.
// <pre>
//   &lt;field name="sourceCurrencyId" foreignKey="currency.id"/&gt;
//   &lt;field name="paymentCurrencyId" foreignKey="currency.id"/&gt;
//   &lt;field name="sourceCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="sourceCurrencyId"/&gt;
//   &lt;field name="paymentCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="paymentCurrencyId"/&gt;
// </pre>
// <p>
// <h3>SQL Templating and <code>includeVia</code></h3>
// <p>
// The <code>includeVia</code> feature uses SQL table aliases in the generated SQL when generating multiple SQL joins
// to the same SQL table.  When using +link{group:customQuerying,SQL Templating}, it's sometimes necessary to know
// the names of the aliases in the generated SQL.  The table alias used can be configured via
// setting +link{dataSourceField.relatedTableAlias} on the <code>foreignKey</code> field, for
// example, using the declarations below, aliases "source" and "payment" would be used for the
// two "currency" tables.
// <pre>
//   &lt;field name="sourceCurrencyId" foreignKey="currency.id" relatedTableAlias="source"/&gt;
//   &lt;field name="paymentCurrencyId" foreignKey="currency.id" relatedTableAlias="payment"/&gt;
//   &lt;field name="sourceCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="sourceCurrencyId"/&gt;
//   &lt;field name="paymentCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="paymentCurrencyId"/&gt;
// </pre>
// <p>
// <h3>Multiple indirection and <code>relatedTableAlias</code></h3>
// <p>
// Sometimes you may have two <code>includeFrom</code> fields that include a field which is itself included from
// another DataSource, for example:
// <pre>
//   &lt;field name="sourceCurId" nativeName="sourceCurrencyId" foreignKey="currency.id" relatedTableAlias="source"/&gt;
//   &lt;field name="sourceCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="sourceCurId"/&gt;
//   &lt;field name="sourceCurrencyGroup" includeFrom="currency.groupName" includeVia="sourceCurId"/&gt;
//   &lt;field name="paymentCurId" nativeName="paymentCurrencyId" foreignKey="currency.id" relatedTableAlias="payment"/&gt;
//   &lt;field name="paymentCurrencySymbol" includeFrom="currency.currencySymbol" includeVia="paymentCurId"/&gt;
//   &lt;field name="paymentCurrencyGroup" includeFrom="currency.groupName" includeVia="paymentCurId"/&gt;
// </pre>
// .. where the "currency" DataSource used above is related to the "currencyGroup" DataSource via fields:
// <pre>
//   &lt;field name="groupId" type="integer" foreignKey="currencyGroup.id" relatedTableAlias="group"/&gt;
//   &lt;field name="groupName" type="text" includeFrom="currencyGroup.groupName"/&gt;
// </pre>
// Fields "sourceCurrencyGroup" and "paymentCurrencyGroup" include a field that is itself an included field.
// In this case "currencyGroup" table will be referenced two times, and its <code>relatedTableAlias</code> defined in
// "currency" DataSource will be prefixed with the <code>includeVia</code> value to make
// aliases unique in generated SQL: "sourceCurId_group" and "paymentCurId_group".
// <p>
// The same aliases would be used if "sourceCurrencyGroup" and "paymentCurrencyGroup" fields
// were to include "currencyGroup.groupName" <i>indirectly</i>:
// <pre>
//   &lt;field name="sourceCurrencyGroup" includeFrom="currency.currencyGroup.groupName" includeVia="sourceCurId"/&gt;
//   &lt;field name="paymentCurrencyGroup" includeFrom="currency.currencyGroup.groupName" includeVia="paymentCurId"/&gt;
// </pre>
// <p>
// This works the same for more complex relationships.  If we add a "moneyTransferDetail"
// DataSource to the sample above which has multiple references to "moneyTransfer"
// DataSource and would include fields from "currency" and "currencyGroup" DataSources:
// <pre>
//   &lt;field name="mtId" nativeName="moneyTransferId" type="integer" foreignKey="moneyTransfer.id" relatedTableAlias="main" /&gt;
//   &lt;field name="mainTransferName" includeFrom="moneyTransfer.name" includeVia="mtId" /&gt;
//   &lt;field name="mainSourceSymbol" includeFrom="moneyTransfer.sourceCurrencySymbol" includeVia="mtId" /&gt;
//   &lt;field name="mainSourceGroup" includeFrom="moneyTransfer.sourceCurrencyGroup" includeVia="mtId" /&gt;
//   &lt;field name="mainPaymentSymbol" includeFrom="moneyTransfer.paymentCurrencySymbol" includeVia="mtId" /&gt;
//   &lt;field name="mainPaymentGroup" includeFrom="moneyTransfer.paymentCurrencyGroup" includeVia="mtId" /&gt;
//   &lt;field name="mtPrevId" nativeName="moneyTransferPreviousId" type="integer" foreignKey="moneyTransfer.id" relatedTableAlias="prev" /&gt;
//   &lt;field name="previousTransferName" includeFrom="moneyTransfer.name" includeVia="mtPrevId" /&gt;
//   &lt;field name="previousSourceSymbol" includeFrom="moneyTransfer.sourceCurrencySymbol" includeVia="mtPrevId" /&gt;
//   &lt;field name="previousSourceGroup" includeFrom="moneyTransfer.sourceCurrencyGroup" includeVia="mtPrevId" /&gt;
//   &lt;field name="previousPaymentSymbol" includeFrom="moneyTransfer.paymentCurrencySymbol" includeVia="mtPrevId" /&gt;
//   &lt;field name="previousPaymentGroup" includeFrom="moneyTransfer.paymentCurrencyGroup" includeVia="mtPrevId" /&gt;
// </pre>
// In this scenario the "currencyGroup" table will be joined 4 times - for all main/prev transfer
// and payment/source currency combinations.  So, aliases will be prefixed with both
// intermediate <code>includeVia</code> values: "mtId_sourceCurId_group",
// "mtId_paymentCurId_group", "mtPrevId_sourceCurId_group", "mtPrevId_paymentCurId_group".
// <p>
// It is also allowed to specify a series of FK fields in <code>includeVia</code>, for example
// "moneyTransferDetail" could declare:
// <pre>
//   &lt;field name="mainSourceCurrencyGroup" includeFrom="moneyTransfer.currency.currencyGroup.groupName" includeVia="mtId.sourceCurId"/&gt;
//   &lt;field name="mainPaymentCurrencyGroup" includeFrom="moneyTransfer.currency.currencyGroup.groupName" includeVia="mtId.paymentCurId"/&gt;
// </pre>
// In this case the prefix used for table aliases will be the <code>includeVia</code> value
// with "_" substituted for ".", so the table aliases will be "mtId_sourceCurId_group" and "mtId_paymentCurId_group".
// <p>
// <b>Note</b> that if +link{dataSourceField.relatedTableAlias,related table alias} is not
// specified, then we don't make any guarantees what alias will be generated.
// <p>
// <b>Note</b> that Oracle has a limit of 30 characters on identifier names.  We limit table
// aliases to 30 characters <b>all databases</b> despite actual database in use to support
// portability across databases.  If the generated table alias would exceed 30 chars,
// we instead use a generated and unpredictable value like "a123".  To avoid hitting this limit
// for the advanced cases discussed above:
// <ul>
// <li> use relatively short strings for <code>relatedTableAlias</code>
// <li> for rare, multi-step inclusion scenarios where multiple field names are used as a
//      prefix, if field names are very long in order to match database column names, you can
//      use a shorter field name and use +link{dataSourceField.nativeName} to specify the
//      underlying column name (this is demonstrated in samples above - note field "mtPrevId")
// </ul>
//
// @see dataSourceField.relatedTableAlias
// @example sqlIncludeVia
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.relatedTableAlias (String : null : IR)
// In +link{group:sqlDataSource,SQL DataSource} for a field that uses
// +link{dataSourceField.foreignKey,foreignKey} specifies table alias name to use in generated SQL.
// <p/>
// Note that using +link{dataSourceField.includeVia, Multiple <code>includeFrom</code> fields referring to the same
// related DataSource} feature can affect aliases in different ways, see its description for details.
// @see dataSourceField.includeVia
// @example sqlIncludeVia
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.displayField (String : null : IR)
// Name of another field in this DataSource that should be used as the display value for this
// field.
// <P>
// Typically used for editable
// +link{DataSourceField.foreignKey,foreignKey} fields: the <code>foreignKey</code> field
// stores an ID value, and this ID value is the right value to use when editing (typically by a
// +link{SelectItem} with +link{SelectItem.optionDataSource,optionDataSource} set).  However,
// when the <code>foreignKey</code> field is viewed read-only, it should display a name, title
// or other friendly value from the related record.  In order to accomplish this, a second,
// hidden field carries the display value, and the <code>foreignKey</code> field has
// <code>displayField</code> set to this second, hidden field.
// <P>
// For a more in-depth discussion, see +link{dataSourceField.includeFrom}.
//
// @serverDS allowed
// @group dataSourceRelations
// @visibility external
//<

//> @attr dataSourceField.joinType (JoinType : null : IR)
// This property is only applicable to fields of SQL DataSources that also specify a
// +link{foreignKey,foreignKey} property; it is ignored for all other fields.  Indicates the
// type of join to make between the tables underlying this DataSource and the other DataSource
// referred to in the <code>foreignKey</code> property, when resolving +link{includeFrom,includeFrom}
// fields.  The default value of null is the same as specifying "inner".
// <P>
// Note, outer joins are allowed for all supported database products only if you are using
// +link{dataSource.useAnsiJoins,ANSI-style joins}, which is not the case by default.  If you
// are using the older strategy of additional join expressions in the WHERE clause, outer
// joins are only supported for database products that provide a proprietary native syntax for
// expressing outer joins.  Those products are:<ul>
// <li>Oracle</li>
// <li>Versions of Microsoft SQL Server earlier than 2012, and running in compatibility mode 80</li>
// </ul>
//
// @serverDS only
// @group dataSourceRelations
// @visibility external
//<


//> @type JoinType
// The type of join to make between two SQL tables when resolving
// +link{dataSourceField.includeFrom,includeFrom} fields.
//
// @value "inner"    A regular inner join, whereby rows are only included in the resultset
//                   where the join can be satisified, so a missing row in the table being
//                   joined to results in the entire row being omitted.
// @value "outer"    An outer join.  All outer joins generated by SmartClient's SQL subsystem
//                   are left outer joins, meaning that every row in the join-from (or "left")
//                   table that matches the criteria is included, and missing rows in the
//                   join-to (or "right") table cause columns to be set to null.
//
// @serverDS only
// @group dataSourceRelations
// @visibility external
//<


// Summary functions
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.summaryFunction (SummaryFunction : null : IR)
// @include listGridField.summaryFunction
// @visibility external
//<

//> @attr dataSourceField.summaryValueTitle (String : null : IR)
// Title to show in a +link{SummaryFunction,Summary of type "title"} for this field. If unspecified
// <code>title</code> summaries will show the +link{dataSourceField.title} for the field.
// @visibility external
//<



// XML binding / serialization
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.xmlAttribute (boolean : null : IR)
// Indicates that +link{dataSource.xmlSerialize()} should serialize this value as an XML
// attribute.
// <P>
// Note this does not need to be declared in order for DataSource records to be derived from
// XML data: a field will be populated with either an attribute or subelement with matching
// name.
//
// @group xmlSerialize
// @group componentSchema
// @serverDS allowed
// @visibility external
//<
//> @attr dataSourceField.childTagName (String : null : IR)
// For a field that is +link{multiple,multiple:"true"}, controls the name of the XML tag used for each
// subelement during +link{dataSource.xmlSerialize()}.
// <P>
// If unset, the default tag name is "value" for a field of simple type, and for a field of
// DataSource type, is the tagName or ID of the DataSource (as though
// <code>xmlSerialize()</code> were called on the child DataSource).
//
// @group xmlSerialize
// @group componentSchema
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.propertiesOnly (boolean : null : IR)
// For use in +link{group:componentSchema} for fields that contain other components, this flag
// suppresses auto-construction for subcomponents that appear under this field.
// <P>
// For example, the +link{VLayout} schema sets this for its +link{Layout.members,members}
// property, so that when a VLayout is constructed via XML as follows:
// <pre>
// &lt;VLayout&gt;
//     &lt;members&gt;
//         &lt;ListGrid ID="myGrid" .../&gt;
//         &lt;Toolstrip ID="myToolStrip" ... /&gt;
//     &lt;/members&gt;
// &lt;/VLayout&gt;
// </pre>
// The ListGrid and ToolStrip do not construct themselves automatically.  Instead, the VLayout
// receives the properties of the ListGrid and ToolStrip as ordinary JavaScript Objects, with
// the special property <code>_constructor</code> set to the name of the class that should be
// constructed.
//
// @group componentSchema
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.inapplicable (boolean : null : IR)
// For use in +link{group:componentSchema}, a field +link{dataSource.inheritsFrom,inherited} from
// another schema can be redeclared with this property set in order to indicate that the
// property should not be used.
// <P>
// This is primarily used to influence +link{group:visualBuilder}.  For simple type properties,
// this avoids the property appearing in the Component Editor.
// <P>
// For fields that hold subcomponents, this prevents inappropriate drag and drop.  For example,
// a custom class called <code>MyDialog</code> may automatically create a series of children,
// and not allow arbitrary other children to be added.  In this case, the inherited property
// +link{Canvas.children} should be marked inapplicable in order to prevent arbitrary
// components being dropped onto a <code>MyDialog</code> instance.
//
// @group componentSchema
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.group (string : null : IR)
// For use in +link{group:componentSchema}, indicates what group to place the property in when
// editing in Visual Builder.
//
// @group componentSchema
// @serverDS allowed
// @visibility external
//<

// Multi-valued fields
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.multiple (boolean : null : IR)
// Indicates that this field should always be Array-valued.  If the value derived from
// +link{dataSource.dataFormat,XML or JSON data} is singular, it will be wrapped in an Array.
// <p>
// JPA and Hibernate DataSources use <code>multiple:true</code> as part of the declaration of
// One-To-Many and Many-to-Many relations - see +link{group:jpaHibernateRelations} for details.
// <p>
// <h4>Criteria on multiple:true fields: client-side filtering</h4>
// <p>
// For simple Criteria, the criteria value is compared to <i>each</i> field value in the
// <code>multiple:true</code> field, according to the
// +link{dsRequest.textMatchStyle,textMatchStyle}.  If <i>any</i> field value matches the
// filter value, the field is considered to match the criteria.
// <p>
// For +link{AdvancedCriteria}, for normal +link{type:OperatorId,search operators} the field
// value is considered as matching the <code>Criterion</code> if <i>any</i> of the field values
// match the Criterion.  Specifically, this is true of all operators that have an
// +link{type:OperatorValueType,operatorValueType} of "fieldType" or "valueRange".
// <p>
// For operators that compare against other fields in same record, such as "equalsField",
// if the other field is <i>not</i> <code>multiple:true</code>, matching works the same as for
// normal operators, that is, as if <code>criterion.value</code> directly contained the value
// rather than the name of another field.
// <p>
// If the other field is <i>also</i> multiple:true, only "equalsField", "notEqualsField",
// "iEqualsField" and "iNotEqualsField" are allowed (any other <code>operator</code> will
// cause a warning and be ignored) and the set of values in the field must be identical (aside
// from case, for operators prefixed with "i") and in identical order to match.
// <p>
// For the <code>inSet</code> operator, the field matches if there is any intersection between
// the field values and the array of values provided in <code>criterion.value</code>.
// <code>notInSet</code> is the reverse.
// <p>
// Finally, for "isNull" and "isNotNull", an empty Array is considered non-null.  For example,
// if you use dataFormat:"json" and the field value is provided to the browser as
// <code>[]</code> (JSON for an empty Array), the field is considered non-null.
// <p>
// <h4>Server-side Representation and Storage</h4>
// <p>
// Values for multiple:true fields appear as Java Lists when received in server code such as a
// DMI.  The SmartClient Server supports simple storage of values that are multiple:true, controlled
// via the +link{multipleStorage} setting.
// <p>
// For server-side behavior of JPA and Hibernate relation fields that are multiple:true, see
// +link{group:jpaHibernateRelations}.
// <p>
// For non-relation fields, the SmartClient Server supports simple storage of values that are
// multiple:true, controlled via the +link{multipleStorage} setting, with some limited support
// for server-side filtering, as described in the +link{multipleStorage} docs.
// <p>
// For the built-in SQL, Hibernate and JPA connectors, if criteria are specified for a
// multiple:true field where <code>multipleStorage</code> is null or "none", the SmartClient
// server knows nothing about how the multiple values are stored, so as a fallback the criteria
// will operate as though the field were a normal, non-multiple "text" field.  This will
// generally <b>not</b> match the client-side filtering behavior described above, so filtering
// should either be performed entirely on the client (for example, via
// +link{listGrid.dataFetchMode,dataFetchMode:"local"} or entirely on the server (via
// +link{resultSet.useClientFiltering}:"false")
// <p>
// The server-side filtering is done through a criteria transform which happens with
// +link{DataSource.transformMultipleFields, transformMultipleFields}.
// <p>
// <h4>XML Serialization</h4>
// <P>
// Specifically for XML serialization and deserialization, <code>multiple:true</code> behaves
// similarly to the
// +externalLink{http://www.google.com/search?hl=en&q=soap+array,SOAP array idiom}, that is,
// there will be a "wrapper element" named after the field name, whose contents will be several
// elements of the specified +link{dataSourceField.type,field.type}.
// <P>
// For example, +link{layout.members} is declared with <code>type:"Canvas",
// multiple:true</code>.  The correct XML format is thus:
// <pre>
// &lt;VLayout&gt;
//     &lt;members&gt;
//         &lt;Canvas ID="myCanvas" ... /&gt;
//         &lt;ListGrid ID="myGrid" .../&gt;
//         &lt;Toolstrip ID="myToolStrip" ... /&gt;
//     &lt;/members&gt;
// &lt;/VLayout&gt;
// </pre>
// <P>
// See +link{dataSourceField.childTagName} for customizing the tagName used for subelements.
//
// @group xmlSerialize
// @group componentSchema
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.multipleValueSeparator (String : ", " : IR)
// For fields that are +link{multiple,multiple:true}, the separator used
// between values when they are displayed.
// @serverDS allowed
// @group multipleField
// @visibility external
//<

//> @type MultipleFieldStorage
// Options for how values are stored for a field that is
// +link{listGridField.multiple,multiple:true}.  See +link{dataSourceField.multipleStorage}.
// @value "simpleString" values are saved as a simple delimeter-separated string.  Delimeter
//        can be configured via +link{dataSourceField.multipleStorageSeparator}.  An empty
//        array is stored as "", and null as the database <code>null</code> value.
// @value "json" values are serialized to JSON.  Empty array as a distinct value
//         from null (it appears as the text "[]").
// @value "none" no transformation is applied to values; server-side field value remains a Java
//        List when passed to the execute(Fetch|Add|Update|Remove) method of the server-side
//        DataSource class
// @group multipleField
// @visibility external
//<

//> @attr dataSourceField.multipleStorage (MultipleFieldStorage : null : IR)
// How values for a +link{multiple,multiple:true} field should be stored.
// <p>
// On the server, field values for <code>multiple:true</code> fields are represented as Java
// Lists in DSRequest and DSResponse objects, but when <code>multipleStorage</code> is used,
// are ultimately stored as Strings.
// <p>
// This storage mechanism is "denormalized" in the sense of
// +externalLink{http://en.wikipedia.org/wiki/Database_normalization,database normalization}
// because the field contains a compound value rather than an atomic value.  Specifically, this
// makes the field harder to use with database features like SQL joins, since its value cannot
// be directly compared to other, singular fields.  For this reason, there's really only a
// narrow set of cases where it makes sense to use this storage mechanism, consider it if:
// <ul>
// <li> the values don't appear in any other tables or columns elsewhere in your database (so
//      joins and other comparisons wouldn't be possible anyway)
// <li> there is no authoritative list of all possible values, or even of popular values; it's
//      basically freeform, like arbitrary, uncurated "tags" applied to photos or links
// </ul>
// <p>
// A <code>multiple:true</code> field can be of any +link{SimpleType}, including "date",
// "time", "datetime", "float", "int" etc.  Type will be preserved across a store and reload
// cycle, so for example a List of values for a field of type "int" will arrive at the server
// in a DSRequest as a List of Integer, be stored as a String, but appear again as a List of
// Integer inside a DSResponse.  When storing "date", "time" or "datetime" values as Strings,
// XML schema formats are used - the same that are used for transmission in XML or JSON (see
// +link{group:dateFormatAndStorage,Date and Time Format and Storage overview} for details).
// <p>
// The <code>multipleStorage</code> feature works by transforming from List to String and back
// inside the server-side DataSource.execute() method.  Specifically, inbound DSRequests are
// transformed before methods that perform actual storage are called (executeUpdate() et al),
// and DSResponses received from these methods have <code>multiple:true</code> field values
// transformed back into Lists before DataSource.execute() returns the DSResponse.
// <p>
// This transformation approach means that the built-in SQL, JPA and Hibernate connectors all
// support <code>multipleStorage</code>, and <code>multipleStorage</code> is also automatically
// available for any custom DataSource that is capable of storing String values, and implements
// its storage methods via overrides of executeUpdate(), executeAdd() et al as described in the
// +link{group:writeCustomDataSource,custom DataSource overview}.
// <p>
// Note that when using the built-in SQL, JPA and Hibernate connectors, the underlying SQL
// column should be of 'text' type.  The +link{group:adminConsole,Admin Console} will
// automatically generate columns of the correct type for a SQLDataSource.  For JPA or
// Hibernate DataSources, just ensure the type of the Java bean property on your Java object is
// String.
// <p>
// The default <code>multipleStorage</code> setting of null means that the "simpleString"
// +link{MultipleFieldStorage} approach will be used if:
// <ul>
// <li> the field is of a known +link{SimpleType}
// <li> the field is not a +link{foreignKey}
// </ul>
// Otherwise, +link{MultipleFieldStorage} "none" will be used.
// <p>
// <h4>Criteria and AdvancedCriteria handling</h4>
// <p>
// For the built-in SQL, JPA and Hibernate connectors, when
// <code>multipleStorage</code>:"simpleString" or "json" is used, criteria are transformed to
// replicate the client-side filtering behavior for multiple:true fields, where possible.
// The following operators are supported with the same behavior as client-side filtering:
// <ul>
// <li> all String-oriented operators including matchesPattern, but not regexp/iRegexp
// <li> isNull / isNotNull
// <li> inSet / notInSet
// <li> equalsField / notEqualsField / iEqualsField / iNotEqualsField
// </ul>
// The following operators, which are supported for client-side filtering of multiple:true
// fields, are not supported for server filtering when using <code>multipleStorage</code>:
// <ul>
// <li> greaterThan/lessThan(OrEqual)
// <li> "between" and all other operators with +link{type:OperatorValueType} of "valueRange"
// <li> regexp / iRegexp as noted above
// </ul>
// Note that for string-based filtering operators such as "equals", no characters which are
// part of the +link{multipleStorageSeparator} may be used in the filter string.  If any
// characters from the <code>multipleStorageSeparator</code> are present in the filter value,
// it will always fail to match.  For "json" mode, the <code>multipleStorageSeparator</code> is
// effectively the String '","'.
//
// @group multipleField
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.multipleStorageSeparator (String : null : IR)
// For fields that are +link{multiple,multiple:true} and use +link{multipleStorage}, the
// separator used in the "simpleString" +link{type:MultipleFieldStorage} mode.
// <p>
// Default value of null means the +link{multipleValueSeparator} is used instead.
//
// @serverDS only
// @group multipleField
// @visibility external
//<

//> @attr operationBinding.transformMultipleFields (boolean : null : IR)
// If set to "false", transformation of values for
// +link{dataSourceField.multiple,multiple:true} fields, normally controlled by
// +link{dataSourceField.multipleStorage}, is instead disabled for this OperationBinding.
//
// @group multipleField
// @serverDS only
// @visibility external
//<

//> @attr dataSource.transformMultipleFields (boolean : null : IR)
// If set to "false", transformation of values for
// +link{dataSourceField.multiple,multiple:true} fields, normally controlled by
// +link{dataSourceField.multipleStorage}, is instead disabled for this entire DataSource.
//
// @group multipleField
// @serverDS only
// @visibility external
//<


// Javabean/POJO Binding
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.javaClass (String : null : IR)
// For use with the SmartClient server when populating Java Beans / POJOs based on data
// contained in a DSRequest, <code>javaClass</code> specifies the fully qualified Java
// className to be created and passed to the setter for the Java Bean Property with the same
// name as this field.   <code>javaClass</code> is used both when manually calling
// DataSource.setProperties() and when auto-populating POJO arguments of a +link{group:dmiOverview,DMI} method.
// <P>
// The Java class to create does not normally have to be specified: SmartClient will
// use Java reflection to inspect the type of argument expected by a setter method and will
// attempt conversion of inbound data to that type.  As described in the documentation for
// DataTools.setProperties(), this works for almost all typical cases.  However
// <code>field.javaClass</code> is useful for:
// <ul>
// <li> subobject of abstract or interface type: in this case Java Reflection is not sufficient
// to discover the concrete type that should be instantiated, and <code>javaClass</code> should be
// set instead.</li>
// <li> subobject of Collection or Map type, when Java generics are not used or the Collection
// member type or Map value type is abstract.  When Java generics are used (for example the
// setter takes an argument is of type Collection&lt;SomePOJO&gt; or Map&lt;KeyType,SomePOJO&gt;,
// SmartClient will automatically attempt to convert inbound data to the type of the members of
// the Collection or values of the Map.  Without generics, <code>javaClass</code> needs to be
// specified.  Note that <code>javaClass</code> will take precedence over generics if both
// are used. Also note that +link{dataSourceField.javaCollectionClass} can be specified if a
// particular Collection or Map type is needed, and +link{dataSourceField.javaKeyClass} can be
// specified for a field of type <code>java.util.Map</code>.</li>
// </ul>
// <b>NOTE:</b> It is also possible to make SmartClient Server determine the javaClass to use
// dynamically at runtime. See the description of <code>DataSource.getPropertyJavaClass()</code>
// in the server documentation for details.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.javaCollectionClass (String : null : IR)
// See +link{dataSourceField.javaClass} - when auto-populating of Java Beans / POJOs based on
// inbound DSRequest data, for a field of type Collection or Map,
// <code>javaCollectionClass</code> can
// be used to specify a particular concrete class to use.  If not specified, and a concrete
// Collection or Map class cannot be discovered using Java Reflection, the following concrete
// classes are used:
// <ul>
// <li> <code>java.util.ArrayList</code> is used for fields of type <code>List</code>
// <li> <code>java.util.HashSet</code> is used for fields of type <code>Set</code>
// <li> <code>java.util.LinkedList</code> is used for fields of type <code>Queue</code>
// <li> <code>org.apache.commons.collections.map.LinkedMap</code> is used for fields of type <code>Map</code>
// <li> <code>java.util.ArrayList</code> is used for fields that are otherwise of type <code>Collection</code>
// </ul>
// Note that this value is used even if the target Collection or Map is declared as a concrete
// class.  So, for example, if you set <code>javaCollectionClass</code> to
// <code>java.util.LinkedList</code> but your setter method accepts a
// <code>java.util.ArrayList</code>, you will get a ClassCastException.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.javaKeyClass (String : null : IR)
// See +link{dataSourceField.javaClass} - when auto-populating of Java Beans / POJOs based on
// inbound DSRequest data, for a field of Map type, <code>javaKeyClass</code> can be used to
// specify a particular concrete class for the map keys.  If not specified, and a concrete
// type cannot be discovered using Java Reflection, <code>java.lang.Object</code> is used.
//
// Note that <code>javaKeyClass</code> take precedence over generics if both are used.
//
// @serverDS only
// @visibility external
//<


// Exporting
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.exportTitle (String : null : IR)
//    Optional different field-title used for exports.
//
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.canExport (Boolean : null : IR)
// Dictates whether the data in this field be exported.  Explicitly setting
// <i>canExport</i> to false overrides the setting on any component-fields, such
// as +link{listGridField.canExport, ListGrid fields}.
//
// @serverDS allowed
// @visibility external
//<

// Upload
// --------------------------------------------------------------------------------------------

//> @attr dataSourceField.uploadFieldName (String : null : IR)
// Used by the +link{BatchUploader} to map a field in an upload file to this
// dataSourceField.  This is only necessary if the dataSourceField's name and title differ
// from the name of the field in the upload file (SmartClient will automatically map upload
// fields using the dataSourceField's title, if possible, if it does not get a direct match
// on field name).
//
// @serverDS allowed
// @visibility batchUploader
//<

//> @attr dataSourceField.showFileInline    (boolean : null : [IR])
// For a field of type:"imageFile", indicates whether to stream the image and display it
// inline or to display the View and Download icons.
//
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.imageWidth (number or String: null : [IR])
// Width of the image-content of this field.  If set as a string, represents the name of
// another field in the record that holds the imageWidth.  Applicable only to fields of image
// type or fields that use a +link{class:ViewFileItem, ViewFileItem} as an editor.
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.imageHeight (number or String: null : [IR])
// Height of the image-content of this field.  If set as a string, represents the name of
// another field in the record that holds the imageHeight.  Applicable only to fields of image
// type or fields that use a +link{class:ViewFileItem, ViewFileItem} as an editor.
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.imageSize (number or String: null : [IR])
// Width and height of the image-content of this field.  If set as a string, represents the
// name of another field in the record that holds the imageSize.  Applicable only to fields
// of image type or fields that use a +link{class:ViewFileItem, ViewFileItem} as an editor.
// @serverDS allowed
// @visibility external
//<


// Role-based security
// ----------------------------------------------------------------------------------------

//> @groupDef fieldLevelAuth
// Properties that provide declarative security features at the level of individual fields
// @title Field-Level Security
// @visibility external
//<

//> @attr dataSourceField.viewRequiresAuthentication (boolean : null : IR)
// Whether a user must be authenticated in order to fetch this field.  If this property is
// set and the user is not authenticated, SmartClient Server will not return values for this
// field to the client.
// <p>
// The way this behavior works is to remove the field from the expressed or implied list of
// fields to return.  A list of fields can be expressed using +link{OperationBinding.outputs}
// or +link{dsRequest.outputs}; in the absence of those, the entire set of fields defined in
// the DataSource is implied.  You can override this behavior on a per-request basis by
// providing a DMI that just re-adds the names of the fields you want for that particular
// request to the "consolidated outputs".  See the server-side Javadoc for
// <code>DSRequest.getConsolidatedOutputs()</code>.
// <p>
// In addition to this behavior of trimming off values in fetched records, a field that fails
// a field-level security check (either this one or one of the two more specific checks,
// +link{viewRequiresRole,viewRequiresRole} and +link{viewRequires,viewRequires}) will be
// altered in the version of the dataSource returned to the client by the
// +link{group:loadDSTag,&lt;loadDS&gt; JSP tag} or
// +link{group:dataSourceDeclaration,DataSourceLoader servlet}.
// <ul>
// <li>If the field is also not editable (see the discussion of this below), it will be marked
// +link{canView,canView}:false, and thus effectively be inert and unusable on the client</li>
// <li>If the field is editable, it will be marked +link{hidden,hidden}.  By default, this means
// that +link{dataBoundComponent}s will not show the field, though this can be overridden for
// individual components.  If you do choose to unhide the field, bear in mind that it will
// never actually show any data, because the underlying security test has still failed</li>
// </ul>
// By default, view constraints automatically imply edit constraints - almost all of the time,
// if a user is not allowed to view a field, he should not be allowed to update it either.  If
// you specify a view constraint, the system behaves as if you had also specified the
// corresponding edit constraint - so <code>viewRequiresAuthentication</code> on a field
// implies <code>+link{dataSourceField.editRequiresAuthentication,editRequiresAuthentication}</code>.
// You override this behavior simply by specifying some kind of edit constraint for the field,
// either a different one (eg, "users" can view a field, but only "managers" can update it) or
// a blank one (if you have some unusual requirement whereby users are not allowed to view a
// field, but they can update it).
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.editRequiresAuthentication (boolean : null : IR)
// Whether a user must be authenticated in order to write to this field.  If this property is
// set and the user is not authenticated, the SmartClient Server will not allow this field to
// be updated or initialized.
// <p>
// This property affects two things.  Firstly, the server determines when the DataSource is
// first loaded if we have an authenticated user; if we don't, the field is marked
// <code>canEdit: false</code>.  Secondly, when an insert or update request is received from
// the client, the server removes the field from the <code>values</code> clause before the
// request is processed.
// <p>
// Note that you can override this behavior.  The <code>canEdit</code> setting can be overridden
// on the the client-side DataSource like any other client-side property.  The value-removing
// behavior can be overridden on a per-request basis by providing a DMI that re-adds values for
// the fields you want for that particular request to the values object (the values originally
// sent up by the client are available on the DSRequest).  See the server-side Javadocs for
// <code>DSRequest.getValues()</code> and <code>DSRequest.getClientSuppliedValues()</code>.
//
// @requiresModules SCServer
// @see dataSourceField.viewRequiresAuthentication
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.initRequiresAuthentication (boolean : null : IR)
// Whether a user must be authenticated in order to initialize to this field.  If this property is
// set and the user is not authenticated, the SmartClient Server will not allow this field to
// be initialized.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.updateRequiresAuthentication (boolean : null : IR)
// Whether a user must be authenticated in order to update to this field.  If this property is
// set and the user is not authenticated, the SmartClient Server will not allow this field to
// be updated.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.viewRequiresRole (String : null : IR)
// Comma-separated list of user roles that are allowed to fetch this field. If the current user
// has any of the roles listed, values for the field will be fetched.  Please see
// +link{operationBinding.requiresRole} for further details of SmartClient's declarative
// role-based security.  Please also see
// +link{dataSourceField.viewRequiresAuthentication,viewRequiresAuthentication} for details
// of how declarative field-level security settings can be overridden per-request.
//
// @requiresModules SCServer
// @see dataSourceField.viewRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.editRequiresRole (String : null : IR)
// Comma-separated list of user roles that are allowed to write to this field. If the current
// user does not have any of the roles listed, the system will not allow this field to be
// initialized or updated.  Please see +link{operationBinding.requiresRole} for further
// details of SmartClient's declarative role-based security.  Please also see
// +link{dataSourceField.editRequiresAuthentication,editRequiresAuthentication} for details
// of how declarative field-level security settings can be overridden per-request.
// <p>
// <b>NOTE:</b> This property prevents both initialization and updates for a field.  If you
// have a need to prevent <em>just</em> initialization or <em>just</em> updates, you can use
// +link{dataSourceField.initRequiresRole} or +link{dataSourceField.updateRequiresRole}.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.viewRequiresRole
// @see dataSourceField.initRequiresRole
// @see dataSourceField.updateRequiresRole
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.initRequiresRole (String : null : IR)
// Comma-separated list of user roles that are allowed to initialize this field. If the current
// user does not have any of the roles listed, the system will not allow this field to be
// initialized.
// <p>
// <b>NOTE:</b> This property only prevents initialization of a field; updates will still be
// allowed.  You should only use this property if you have a special requirement; ordinarily,
// use +link{dataSourceField.editRequiresRole}, which applies security for both types of write.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @see dataSourceField.updateRequiresRole
// @see dataSourceField.initRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.updateRequiresRole (String : null : IR)
// Comma-separated list of user roles that are allowed to update this field. If the current
// user does not have any of the roles listed, the system will not allow this field to be
// updated.
// <p>
// <b>NOTE:</b> This property only prevents a field from being updated; initialization will
// still be allowed.  You should only use this property if you have a special requirement;
// ordinarily, use +link{dataSourceField.editRequiresRole}, which applies security for both
// types of write.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @see dataSourceField.initRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.viewRequires (VelocityExpression : null : IR)
// Indicates that the specified +link{type:VelocityExpression} must evaluate to true if
// values for the field are to be fetched.  If the specified expression does not evaluate
// to true, the field will be dropped as described for
// +link{dataSourceField.viewRequiresAuthentication,viewRequiresAuthentication}.
// <p>
// In addition to the normal context variables available to Velocity expressions in SmartClient,
// expressions you write for field-level <code>requires</code> clauses - <code>viewRequires</code>,
// +link{editRequires,editRequires}, +link{initRequires,initRequires} and
// +link{updateRequires,updateRequires} - can reference two additional variables:
// <code>$fieldName</code> and <code>$dsName</code>.  These are the names of the dataSource and
// field currently undergoing <code>requires</code> checks.  They are helpful because they allow
// you to write a generic checker function that can be used to handle <code>requires</code>
// checks for multiple fields and dataSources.
//
// @requiresModules SCServer
// @see dataSourceField.viewRequiresAuthentication
// @see dataSourceField.viewRequiresRole
// @see dataSourceField.editRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.editRequires (VelocityExpression : null : IR)
// Indicates that the specified +link{type:VelocityExpression} must evaluate to true if
// writes to this field are to be allowed.  If the specified expression does not evaluate
// to true, the field will be removed from the request as described for
// +link{dataSourceField.editRequiresAuthentication,editRequiresAuthentication}.
// <p>
// In addition to the normal context variables available to Velocity expressions in SmartClient,
// expressions you write for field-level <code>requires</code> clauses - <code>editRequires</code>,
// +link{viewRequires,viewRequires}, +link{initRequires,initRequires} and
// +link{updateRequires,updateRequires} - can reference two additional variables:
// <code>$fieldName</code> and <code>$dsName</code>.  These are the names of the dataSource and
// field currently undergoing <code>requires</code> checks.  They are helpful because they allow
// you to write a generic checker function that can be used to handle <code>requires</code>
// checks for multiple fields and dataSources.
// <p>
// <b>NOTE:</b> This property prevents both initialization and updates for a field.  If you
// have a need to prevent <em>just</em> initialization or <em>just</em> updates, you can use
// +link{dataSourceField.initRequires} or +link{dataSourceField.updateRequires}.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @see dataSourceField.viewRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.initRequires (VelocityExpression : null : IR)
// Indicates that the specified +link{type:VelocityExpression} must evaluate to true if
// initializations of this field are to be allowed.  If the specified expression does not
// evaluate to true, the field will be removed from the request as described for
// +link{dataSourceField.editRequiresAuthentication,editRequiresAuthentication}.
// <p>
// In addition to the normal context variables available to Velocity expressions in SmartClient,
// expressions you write for field-level <code>requires</code> clauses - <code>initRequires</code>,
// +link{viewRequires,viewRequires}, +link{editRequires,editRequires} and
// +link{updateRequires,updateRequires} - can reference two additional variables:
// <code>$fieldName</code> and <code>$dsName</code>.  These are the names of the dataSource and
// field currently undergoing <code>requires</code> checks.  They are helpful because they allow
// you to write a generic checker function that can be used to handle <code>requires</code>
// checks for multiple fields and dataSources.
// <p>
// <b>NOTE:</b> This property only prevents initialization of a field; updates will still be
// allowed.  You should only use this property if you have a special requirement; ordinarily,
// use +link{dataSourceField.editRequires}, which applies security for both types of write.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @see dataSourceField.viewRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.updateRequires (VelocityExpression : null : IR)
// Indicates that the specified +link{type:VelocityExpression} must evaluate to true if
// updates to this field are to be allowed.  If the specified expression does not
// evaluate to true, the field will be removed from the request as described for
// +link{dataSourceField.editRequiresAuthentication,editRequiresAuthentication}.
// <p>
// In addition to the normal context variables available to Velocity expressions in SmartClient,
// expressions you write for field-level <code>requires</code> clauses - <code>updateRequires</code>,
// +link{viewRequires,viewRequires}, +link{editRequires,editRequires} and
// +link{initRequires,initRequires} - can reference two additional variables:
// <code>$fieldName</code> and <code>$dsName</code>.  These are the names of the dataSource and
// field currently undergoing <code>requires</code> checks.  They are helpful because they allow
// you to write a generic checker function that can be used to handle <code>requires</code>
// checks for multiple fields and dataSources.
// <p>
// <b>NOTE:</b> This property only prevents update of a field; initialization will still be
// allowed.  You should only use this property if you have a special requirement; ordinarily,
// use +link{dataSourceField.editRequires}, which applies security for both types of write.
//
// @requiresModules SCServer
// @see dataSourceField.editRequiresAuthentication
// @see dataSourceField.editRequiresRole
// @see dataSourceField.viewRequires
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.creatorOverrides (boolean : null : IR)
// Indicates that normal declarative security rules specified on this field are waived for
// rows that were created by the current user, as described in the discussion of
// +link{DataSource.creatorOverrides,dataSource.creatorOverrides}.  This setting overrides
// <code>dataSource.creatorOverrides</code>, for this field only.
// <p>
// Note that field-level <code>creatorOverrides</code> is conceptually slightly different to
// the setting at DataSource and OperationBinding levels.  To give the example of a fetch
// operation, at the other two levels, it results in a limited set of rows being returned,
// rather than a security exception being thrown.  At the field-level, it results in values
// for individual fields being present in those rows, rather than them being stripped out on
// the server.
//
// @requiresModules SCServer
// @see dataSourceField.editRequires
// @see dataSourceField.viewRequires
// @see dataSource.creatorOverrides
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<


// Auditing
// ---------------------------------------------------------------------------------------------

//> @attr dataSource.audit (boolean : false : [IR])
// Enables saving of a log of changes to this DataSource in a second DataSource with the same
// fields, called the "audit DataSource".  <b>NOTE</b>: this feature applies to Enterprise
// Edition only; for more information on edition-specific features, see
// +externalLink{http://smartclient.com/product}.
// <p>
// When auditing is enabled, every time a DSRequest modifies this DataSource, a Record is added
// to the audit DataSource that shows the record as it existed after the change was made (or
// for a "remove", the values of the record at the time of deletion).  In addition, the audit
// DataSource has the following additional metadata fields:
// <ul>
// <li> +link{auditTypeFieldName,"audit_operationType"}: type of the change ("update", "add" or "remove")
// <li> +link{auditUserFieldName,"audit_modifier"}: username of the user that made the change.  The username is
//      determined in the same way that the
//      +link{operationBinding.requiresRole,Declarative Security} subsystem determines the
//      current user.
// <li> +link{auditTimeStampFieldName,"audit_changeTime"}: a field of type "datetime" recording the timestamp of
//      the change
// <li> +link{auditRevisionFieldName,"audit_revision"}: a field of type "sequence" recording a simple
//      increasing integer value
// </ul>
// <p>
// If any of the field names above collide with field names of the DataSource being audited,
// an integer suffix will also be added, starting with 2 (for example, "audit_modifier2", then
// "audit_modifier3", etc).
// <p>
// To omit a data field from the automatically generated audit DataSource, just set
// +link{dataSourceField.audit} to false.
// <p>
// <h4>Auto-generated Audit DataSources</h4>
// <p>
// The audit DataSource is normally automatically generated, and unless otherwise specified
// with +link{dataSource.auditDataSourceID}, the ID of the audit DataSource will be
// <code>audit_[OriginalDSID]</code>.
// <p>
// By default, the automatically generated audit DataSource will be of the same type as the
// DataSource being audited, however, if the DataSource being audited is not already a
// SQLDataSource, we recommend using +link{auditDSConstructor,auditDSConstructor:"sql"} to use
// a SQLDataSource as the audit DataSource.  This is because a SQLDataSource used an audit
// DataSource will automatically generate a SQL table for storing audit data the first time
// changes are made.  JPA would require manual creation of a Java Bean, and Hibernate requires
// +externalLink{http://www.google.com/search?q=hbm2ddl.auto,hbm2ddl.auto=update} to be set,
// which is widely considered unsafe for production use.
// <p>
// Automatically created audit DataSources can be loaded and queried just like other
// DataSources, using the DataSourceLoader, and using the server-side API
// <code>DataSource.getAuditDataSource()</code>.  However, you <b>must</b> load the DataSource
// being audited before loading its automatically created audit DataSource.
// <p>
// Note, that automatic SQL tables creation can be disabled. See
// +link{dataSource.autoCreateAuditTable,autoCreateAuditTable} for details.
// <p>
// <h4>Manually created Audit DataSources</h4>
// <p>
// The audit DataSource can also be manually created.  In this case, you can
// can either follow the naming conventions described above for the ID of the audit DataSource
// and the names of metadata fields, or use the linked properties to assign custom names.  If
// you omit any data fields from the tracked DataSource in your audit DataSource, those fields
// will be ignored for auditing purposes, exactly as though +link{dataSourceField.audit} had
// been set to false for an automatically-generated audit DataSource.
// <p>
// Also, note that in case of manually defined audit DataSource, if this DataSource
// is defined so it inherits the audited DataSource, all the audited DataSource's fields
// will be inherited, this including the primary keys. Since for the audit
// DataSource the primary key should be the revision field, in order to prevent the
// audit DataSource having two primary keys, the inherited DataSource's primary key
// will have to be declared in audit DataSource, but with the primaryKey attribute
// omitted (as well not being of type "sequence") in the audit DataSource.
//
// @group serverDataIntegration
// @requiresModules SCServer
// @example auditing
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.audit (boolean : true : [IR])
// Setting <code>audit</code> to <code>false</code> explicitly indicates that this field will
// not be saved to the audit DataSource when +link{dataSource.audit,auditing} is enabled.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditDataSourceID (string : null : [IR])
// For DataSources with +link{audit,auditing enabled}, optionally specifies the ID of the audit
// DataSource.  If this property is not specified, the ID of the audit DataSource will
// be <code>audit_[OriginalDSID] </code>
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditUserFieldName (string : "audit_modifier" : [IR])
// For DataSources with +link{audit,auditing enabled}, specifies the field name used to store
// the user who performed the operation.  If empty string is specified as the field name, the
// audit DataSource will not store this field.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditTimeStampFieldName (string : "audit_changeTime" : [IR])
// For DataSources with +link{audit,auditing enabled}, specifies the field name used to store
// the timestamp when the operation was performed (in a field of type "datetime").  If empty
// string is specified as the field name, the audit DataSource will not store this field.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditRevisionFieldName (string : "audit_revision" : [IR])
// For DataSources with +link{audit,auditing enabled}, specifies the field name used to store
// the revision number for the change (in a field of type "sequence").  If empty
// string is specified as the field name, the audit DataSource will not store this field.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditTypeFieldName (string : "audit_operationType" : [IR])
// For DataSources with +link{audit,auditing enabled}, specifies the field name used to store
// the +link{type:DSOperationType,operationType} (in a field of type "text").  If empty
// string is specified as the field name, the audit DataSource will not store this field.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.auditDSConstructor (String : null : [IR])
// For DataSources with +link{audit,auditing enabled}, optionally specifies the
// +link{dataSource.serverConstructor} for the automatically generated audit DataSource.  The
// default is to use the same <code>serverConstructor</code> as the DataSource where
// <code>audit="true"</code> was declared.
// <p>
// This property is primarily intended to allow the use of SQLDataSource
// (+link{DataSource.serverType,serverType:"sql"}) as an audit DataSource for a DataSource that
// might be of another type.  For example, you might have a DataSource that implements all CRUD
// operations via Java logic in +link{group:dmiOverview,DMI declaration} methods, and so doesn't provide generic
// storage; by using SQLDataSource as the type of your audit DataSource, you don't need to
// implement your own scheme for storing and querying audit data, and the necessary audit
// tables will be automatically generated in the database.
// <p>
// Similarly, even if you do use a reusable DataSource type such as the built-in JPADataSource,
// using SQLDataSource for audit DataSources means there's no need to write a JPA bean just to
// achieve storage of an audit trail.
// <p>
// To simplify this intended usage, the string "sql" is allowed for
// <code>auditDSConstructor</code> as a means of specifying that the built-in SQLDataSource class
// should be used.  For any other type, use the fully qualified Java classname, as is normal
// for <code>serverConstructor</code>.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.autoCreateAuditTable (boolean : true : [IR])
// Setting <code>autoCreateAuditTable</code> to <code>true</code> indicates that audit DataSource
// will automatically create SQL table when +link{dataSource.audit,auditing} is enabled.
// <p>
// Note, that <code>autoCreateAuditTable</code> attribute takes effect only if framework setting
// <code>audit.autoCreateTables</code> in <code>server.properties</code> is set to <code>false</code>,
// which by default is set to <code>true</code>.
//
// @serverDS only
// @visibility external
//<

// Miscellaneous
// --------------------------------------------------------------------------------------------

//> @attr dataSourceField.customSQL (boolean : null : IR)
// For a DataSource with +link{dataSource.serverType,serverType} "sql" or "hibernate",
// indicates that this field should be omitted by default from all SQL or Hibernate operations,
// and will only be used with +link{group:customQuerying,custom queries}.
// <P>
// Having marked a field as <code>customSQL</code> you can refer to it via
// $criteria.<i>fieldName</i> or $values.<i>fieldName</i> in customized queries.
// <P>
// The following are situations where you would <b>not</b> use <code>customSQL</code>:
// <ul>
// <li>simple joins where you want to enable users to see and search on a field from another
// table; consider +link{dataSourceField.includeFrom} instead
// <li>fields where you want to calculate or transform values in SQL on load or save, but
// always perform the same calculation for each operationType; consider instead
// +link{sqlStorageStrategy} for some common cases, or
// +link{customSelectExpression}, +link{customUpdateExpression} and
// +link{customInsertExpression} for full customization
// <li>a special fetch is needed where the field needs to be excluded from the
// $defaultWhereClause so that it can be used in a custom &lt;whereClause&gt; - consider
// +link{operationBinding.excludeCriteriaFields} instead
// </ul>
// <P>
// Use customSQL in situations like:
// <ul>
// <li>there are multiple variations of the "fetch" operation with different
// +link{operationBinding.operationId,operationIds}, and the field is only used in some of them;
// in that case, consider using +link{operationBinding.customFields} to selectively re-introduce
// SQL generation for the field only in operations where it's used.
// <li>the field represents hidden criteria on a field in another table where the field is
// never shown to the user
// <li>the field is a write-only value only saved in some operations
// <li>more than one data access strategy is in use (eg direct SQL for fetch and bean-based
// persistence accessed via DMI for saves) and certain fields are not available in SQL
// </ul>
//
// @serverDS allowed
// @visibility customSQL
//<


//>
// NOTE: This used to be customSQLExpression; we renamed it to customSelectExpression when we
// introduced customUpdateExpression and customInsertExpression, to avoid ambiguity.  The
// system still supports customSQLExpression, for backompat
//<

//> @attr dataSourceField.customSelectExpression (string : null : IR)
// This property indicates that this field represents a custom expression that should be
// embedded in the generated SQL instead of a reference to this field.  For example, if
// you have a field <code>partialName</code> where this value is set to
// <code>SUBSTR(surname, 2)</code>, the generated SQL would look similar to this:
// <pre>
//   SELECT ... SUBSTR(surname, 2) AS partialName ...
// </pre>
// Note that adding "AS partialName" as shown above is required - this is how the framework
// identifies the expression as producing a value for this particular DataSourceField when
// results are received from the database.
// <p>
// Fields with <code>customSelectExpression</code> set can be used for sorting and filtering in
// the normal way, but they are only applicable to update-type operations if you also provide a
// corresponding +link{customUpdateExpression,customUpdateExpression} and/or
// +link{customInsertExpression,customInsertExpression}.  See the documentation
// for those methods for the rules of how they are applied.
// <p>
// You can use Velocity expressions in your <code>customSelectExpression</code>s, and the
// +link{group:velocitySupport,usual context variables} are available.  Note that the results
// of Velocity evaluations are automatically quoted and escaped by default; this behavior can
// be switched off - see +link{autoQuoteCustomExpressions,autoQuoteCustomExpressions}, and
// particularly note the warnings about its use.
// <p>
// Note that this property only applies to users of the SmartClient server using dataSources of
// type "sql".
//
// @see customUpdateExpression
// @see customInsertExpression
// @see customCriteriaExpression
// @see autoQuoteCustomExpressions
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.customUpdateExpression (string : null : IR)
// This property specifies the value to use to update this column on "update" operations.
// The value of this property will be passed through Velocity evaluation and then embedded
// directly in the SQL generated to perform the update.  It can be used in conjunction with
// +link{customSelectExpression,customSelectExpression} to provide bi-directional mapping
// between application data formats and persisted data formats.  Or, it can be used
// unilaterally as a means of silently enforcing data rules - for example, ensuring that all
// values for a given field are in upper case.
// <P>
// You can use Velocity expressions in your <code>customUpdateExpression</code>s, and the
// +link{group:velocitySupport,usual context variables} are available.  Note that the results
// of Velocity evaluations are automatically quoted and escaped by default; this behavior can
// be switched off - see +link{autoQuoteCustomExpressions,autoQuoteCustomExpressions}, and
// particularly note the warnings about its use.
// <p>
// The value must be a string that will end up as a valid SQL snippet after Velocity
// evaluation.  The following examples are valid:
// <ul>
// <li><code>"'Hello world'"</code></li>
// <li><code>"$values.someTextField.toUpperCase()"</code></li>
// <li><code>"SUBSTR(ANOTHER_COLUMN, 1, 10)"</code></li>
// <li><code>"SUBSTR($values.someTextField.toUpperCase(), 1, 10)"</code></li>
// <li><code>"SOME_NUMERIC_COLUMN * 1000"</code></li>
// <li><code>"$values.someDateField.getMonth() + 1"</code></li>
// </ul>
// The first example is a literal and so it must be enclosed in quotes to render valid SQL.
// The second example is also a literal, but as it is the result of a Velocity evaluation, it
// will be automatically quoted as appropriate.  The third example is an actual SQL snippet
// and does not need to be enclosed in quotes (indeed, would fail if it were).  The fourth
// example shows an SQL snippet containing a literal - again, because the literal is the result
// of a Velocity evaluation, it will be automatically quoted.  The remaining two examples both
// yield literals, but as they are numeric literals they must not be quoted.
// <P>
// When an "update" operation runs, any fields that specify
// +link{customSelectExpression,customSelectExpression} but do <b>not</b> specify
// customUpdateExpression will be ignored.  When an "add" operation runs, this property acts
// as a default for any fields that do not specify a
// +link{customInsertExpression,customInsertExpression}; similar to update, any fields that
// specify a customSelectExpression but do not specify either a customUpdateExpression or
// customInsertExpression, will be ignored when "add" operations run.
// <p>
// Note that this property only applies to users of the SmartClient server using dataSources of
// type "sql".
//
// @see customSelectExpression
// @see customInsertExpression
// @see customCriteriaExpression
// @see autoQuoteCustomExpressions
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.customInsertExpression (string : null : IR)
// This property is similar to +link{customUpdateExpression,customUpdateExpression}; its value
// is used during "add" operations.  If you do not specify this property, but do specify a
// <code>customUpdateExpression</code>, the <code>customUpdateExpression</code> is used instead.
// <P>
// Everything that applies to <code>customUpdateExpression</code> also applies to this property,
// including the observation that fields which specify a
// +link{customSelectExpression,customSelectExpression} but no corresponding
// <code>customUpdateExpression</code> or <code>customInsertExpression</code>, will be ignored
// when adding new records.
// <p>
// Note that this property only applies to users of the SmartClient server using dataSources of
// type "sql".
//
// @see customUpdateExpression
// @see customSelectExpression
// @see customCriteriaExpression
// @see autoQuoteCustomExpressions
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.customCriteriaExpression (string : null : IR)
// This property indicates that this field should be represented by a custom expression
// embedded in the WHERE clause of the generated SQL, instead of the generated expression
// containing this field name that would ordinarily be used.  You use this property when
// you have to accomodate some special way of using a field's value in criteria, other than by
// directly comparing it to the criteria.  For example, if you have a column that contains
// bit-packed information you will generally need to perform a bitwise AND to filter on that
// column, rather than an equality check.  In this case, you would use a
// <code>customCriteriaExpression</code> something like this (in Oracle):
// <p>
// <code>&nbsp;&nbsp;&nbsp;BITAND(myField, $criteria.myField)+0 = $criteria.myField</code>
// <p>
// Or this (in MySQL or SQL Server):
// <p>
// <code>&nbsp;&nbsp;&nbsp;myField & $criteria.myField = $criteria.myField</code>
// <p>
// As this example shows, a <code>customCriteriaExpression</code> is expected to be a complete
// logical expression that provides both sides of the comparison.  After Velocity processing,
// this example would result in SQL similar to this (for the MySQL case); the colored part
// comes entirely from the <code>customCriteriaExpression</code>:
// <p>
// <code>SELECT myField, myOtherField FROM myTable WHERE <font color='blue'>myField & 32 = 32</font></code>
// <p>
// <code>customCriteriaExpression</code> can also be used with <code>AdvancedCriteria</code>.
// Note that the point mentioned above, about a <code>customCriteriaExpression</code> being a
// complete logical expression that provides both sides of the comparison still applies.  This
// means that when you use <code>customCriteriaExpression</code> in conjunction with
// <code>AdvancedCriteria</code>, you effectively switch off support for different
// +link{type:OperatorId}s to be used on that field, unless you use Velocity expressions
// containing the special <code>$criteriaOperator</code> variable (see below).
// <p>
// When you use <code>customCriteriaExpression</code> with <code>AdvancedCriteria</code>, the
// way you access criteria values differs slightly.  One option is to use the
// <code>$advancedCriteria</code> Velocity variable, as described in the "Using AdvancedCriteria"
// section of the +link{group:customQuerying,custom querying overview}.  However, this
// variable only returns the value from the first +link{object:Criterion} that uses the field,
// as found by depth-first search.  If your <code>AdvancedCriteria</code> contains multiple
// references to this field, this will not be satisfactory.  Therefore, specifically for
// <code>customCriteriaExpression</code>, we provide another Velocity variable,
// <code>$criteriaValue</code>.  This variable means "the value on a Criterion referencing
// this field".  An example may serve to clarify this:
// <p>
// Assume you need to extend the above example to filter records where a certain bit is set
// OR a certain other bit is set.  An <code>AdvancedCriteria</code> like this would be
// required (note that the choice of "equals" as the operator in these clauses is completely
// arbitrary - as discussed above, the operator is ignored when we have a
// <code>customCriteriaExpression</code>, so it would make no difference to the end product
// if we had used different operators here):<pre>
//   { _constructor: "AdvancedCriteria", operator: "or", criteria: [
//       { fieldName: "myField", operator: "equals", value: 32 },
//       { fieldName: "myField", operator: "equals", value: 64 },
//     ]
//   }
// </pre>
// If you used the <code>customCriteriaExpression</code> from above unchanged with this criteria,
// the following SQL would be generated (in the MySQL case):
// <p>
// <code>SELECT myField, myOtherField FROM myTable WHERE myField & null = null OR myField & null = null</code>
// <p>
// This is because <code>$criteria</code> is a variable that only works correctly with simple
// criteria.  If you changed the <code>$criteria</code> references in the expression to
// <code>$advancedCriteria</code>, the generated SQL would be:
// <p>
// <code>SELECT myField, myOtherField FROM myTable WHERE myField & 32 = 32 OR myField & 32 = 32</code>
// <p>
// This is because <code>$advancedCriteria</code> always returns the first value it finds for
// a particular field, as described above.  However, if you change the expression so it reads:
// <p>
// <code>&nbsp;&nbsp;&nbsp;myField & $criteriaValue = $criteriaValue</code>
// <p>
// you will now get the correct SQL generated:
// <p>
// <code>SELECT myField, myOtherField FROM myTable WHERE myField & 32 = 32 OR myField & 64 = 64</code>
// <p>
// Similar to <code>$criteriaValue</code>, we provide a special variable,
// <code>$criteriaOperator</code>.  This is intended for use in Velocity expressions, so that
// your <code>customCriteriaExpression</code> can process different operators in different ways.
// How you choose to interpret an operator is entirely up to you; this variable is both
// advanced and rarely used.  This example is contrived and does not show a usage that would be
// remotely likely in a real application, but it does show how to use the variable in a Velocity
// expression:
// <p>
// <code>&nbsp;&nbsp;&nbsp;myField #if ($criteriaOperator == "greaterThan") & #else | #end $criteriaValue = $criteriaValue</code>
// <p>
// For simple criteria, note that <code>$criteriaOperator</code> will vary depending on field
// type and the +link{type:TextMatchStyle,textMatchStyle} in force, as follows:<ul>
// <li>Text fields with textMatchStyle "substring" - "iContains"</li>
// <li>Text fields with textMatchStyle "startsWith" - "iStartsWith"</li>
// <li>Text fields with textMatchStyle "exact" - "iEquals"</li>
// <li>All other cases (including text fields with textMatchStyle "exactCase") - "equals"</li>
// </ul>
// You can use Velocity expressions in your <code>customCriteriaExpression</code>s, and the
// +link{group:velocitySupport,usual context variables} are available.  Note that the results
// of Velocity evaluations are automatically quoted and escaped by default; this behavior can
// be switched off - see +link{autoQuoteCustomExpressions,autoQuoteCustomExpressions}, and
// particularly note the warnings about its use.
// <p>
// Note that this property only applies to users of the SmartClient server using dataSources of
// type "sql".
//
// @see customSelectExpression
// @see customUpdateExpression
// @see customInsertExpression
// @see autoQuoteCustomExpressions
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.autoQuoteCustomExpressions (boolean : true : IR)
// If explicitly set to false, switches off automatic quoting and escaping of values in
// custom expressions that are derived from Velocity evaluations.
// <p>
// By default, any part of a +link{customSelectExpression,customSelectExpression},
// +link{customCriteriaExpression,customCriteriaExpression},
// +link{customUpdateExpression,customUpdateExpression} or
// +link{customInsertExpression,customInsertExpression} that comes ultimately from evaluating
// a Velocity expression, will be automatically quoted and escaped according to the syntax
// of the underlying database.  We do this because "raw" values are vulnerable to
// <a href="http://en.wikipedia.org/wiki/SQL_injection">SQL injection attacks</a>.
// <p>
// However, it is possible that the exact SQL logic you desire for a custom expression cannot
// be expressed without access to the raw, unquoted value.  To work around this, you can either
// use the special +link{group:velocitySupport,$rawValue} context variable, or you can set this
// property to false to switch off auto-quoting completely for all custom expressions on this
// field.
// <P>
// <b>Warning</b>:  Bear in mind that it is <b>dangerous</b> to use raw values.  There are some
// cases where using the raw value is necessary, but even so, all such cases are likely to be
// vulnerable to injection attacks.  Generally, the presence of <code>$rawValue</code> in a
// custom expression, or any fields with <code>autoQuoteCustomExpressions: false</code>
// specified, should be viewed as a red flag.
//
// @see customSelectExpression
// @see customUpdateExpression
// @see customInsertExpression
// @see customCriteriaExpression
// @serverDS only
// @visibility customSQL
//<


//> @attr dataSourceField.nativeName (string : null : IR)
// Only applicable to "sql" dataSources. If set, this property indicates that the field is
// bound to the column named in the property.  It is used to bind a DataSourceField to a
// database column with a different name.
// <p>
// <code>nativeName</code> is useful for disambiguating in cases where you have customized
// SQL.  For example, assume you are joining two tables - call them <code>product</code> and
// <code>supplier</code> - that both contain a column called "name".  Only one field in the
// DataSource can be called "name"; to disambiguate, we need to specify both
// +link{dataSourceField.tableName,tableName} and <code>nativeName</code>.  A field declaration
// like this would be needed:<br>
// <pre>    &lt;field name="supplierName" type="text" tableName="supplier" nativeName="name" /></pre>
// <p>
// You may also wish to use this property to map legacy column names to more meaningful field
// names in the DataSource.  For example:<br>
// <pre>    &lt;field name="productName" type="text" nativeName="PRDNM" /></pre>
//
// @see tableName
// @serverDS only
// @visibility customSQL
//<


//> @attr dataSourceField.tableName (String : null : IR)
// Only applicable to "sql" dataSources, setting <code>tableName</code> indicates that this
// field will be coming from another table by way of a
// +link{group:customQuerying,customized SQL query}.
// <P>
// By default, the SQL generator will <b>not</b> include this field in generated SQL for "add"
// or "update" operations.  It will include the field in the SELECT clause and WHERE clause of
// customized "fetch" operations, where it is expected that you will make the table available
// via a custom &lt;tableClause&gt; as shown in +explorerExample{largeValueMapSQL,this sample}.
// <P>
// The table name will also be used to qualifying the column name unless
// +link{DataSource.qualifyColumnNames} or +link{OperationBinding.qualifyColumnNames} has been
// disabled.
// <p>
// <b>Note:</b> This property is only required when including fields from tables other than the
// default table specified by +link{DataSource.tableName}.
//
// @see nativeName
// @serverDS only
// @visibility customSQL
// @example largeValueMapSQL
//<

//> @method dataSourceField.exportForceText (ForceTextApproach : null : IR)
// When using +link{dataSource.recordsAsText}, what approach (if any) should be used to force
// values to be intepreted as text instead of heuristically parsed as dates, times or other
// structured types.
// @visibility external
//<

//> @attr dataSourceField.sqlStorageStrategy (String : null : IR)
// For certain field types, indicates the strategy to be used to store values to the underlying
// SQL column.
// <P>
// <b>Fields of type "boolean"</b><br>
// The default strategy for boolean fields is to assume the underlying type of the field is
// text and store boolean true and false values as the character strings "true" and "false".
// The following additional strategies are available:
// <ul>
// <li>"number" or "integer": store true as 1, false as 0
// <li>"singleChar10": store true as "1", false as "0"
// <li>"singleCharYN": store true as "Y", false as "N"
// <li>"singleCharTF": store true as "T", false as "F"
// <li>"singleChar": store as a single character, using the values defined by
// +link{dataSourceField.sqlTrueValue,sqlTrueValue} and
// +link{dataSourceField.sqlFalseValue,sqlFalseValue}
// <li>"string": store the entire values defined by
// +link{dataSourceField.sqlTrueValue,sqlTrueValue} and
// +link{dataSourceField.sqlFalseValue,sqlFalseValue}
// </ul>
// <P>
// <b>Fields of type "date", "time", and "datetime"</b><br>
// The default strategy for these types is to use a native type that supports date and
// time values to the nearest second; the actual type used varies by database.  The following
// additional strategies are available:
// <ul>
// <li>"nativeDate": Use a native SQL DATE type</li>
// <li>"number" or "integer": Store the value as a plain numeric value</li>
// <li>"text": Store the value as a plain text value</li>
// </ul>
// The "number" and "text" strategies for date/time/datetime fields are provided to allow transparent
// two-way mapping of legacy date values; you would not ordinarily use them in the absence of a
// legacy database.  In both cases, the actual format of the number or text string is determined
// by specifying a +link{dataSourceField.sqlDateFormat,sqlDateFormat} on the field; if no
// format is specified, the framework defaults to "yyyyMMdd".
// <P>
// <P>
// <b>Fields of type "text"</b><p>
// <em>NOTE: This section is only applicable for users of Microsoft SQL Server.  The "ntext"
// strategy is ignored for all other databases.</em><p>
// If you are integrating with Microsoft SQL Server, you may run into an issue with that database
// and double-byte character sets.  Although the documentation indicates that string values are
// sent to the database as Unicode by default, they may undergo conversion at the database end,
// depending on the collation settings of the database.
// <a href=http://support.microsoft.com/kb/239530>This Microsoft support article</a> describes
// the situation, which is that Unicode is converted to the code page of the database before
// being used, in order to "provide backward compatibility with existing applications".
// <P>
// To work around this, we provide an <code>sqlStorageStrategy</code> of "ntext".  Fields that
// declare this strategy are inserted into queries using the "N syntax", like so:<br>
// <code>&nbsp;&nbsp;&nbsp;UPDATE MyTable SET MyTextColumn = N'some text' WHERE MyPK = N'some key'</code>
// <P>
// This syntax overrides defaults and forces the database to treat strings as Unicode.
// <P>
// Note that declaring a field to be of +link{type:FieldType,type} "ntext" is exactly
// equivalent to specifying type "text" and an <code>sqlStorageStrategy</code> of "ntext".
// If you declare a field of type "ntext" when the back-end database is something other than
// SQL Server, it will be treated as a normal field of type "text".
// <p>
// This property only applies to users of the SmartClient server using dataSources of
// type "sql".
//
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.sqlDateFormat (String : null : IR)
// For fields of type "date", "time", and "datetime" that specify an
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "number" or "text", this
// property allows you to specify the format SmartClient should use to map to/from the
// underlying value stored in the database.  The string provided must be a valid
// <a href=http://download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html>
// SimpleDateFormat pattern string</a>.
// <P>
// For fields of type "datetime" and "time" that specify an
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "number", two special
// sqlDateFormat values are supported:
// <ul>
//   <li>"epoch" - The datetime or time value is stored as the number of full seconds that have
//       elapsed since January 1, 1970, 00:00:00 GMT.</li>
//   <li>"epochms" - The datetime or time value is stored as the number of milliseconds that
//       have elapsed since January 1, 1970, 00:00:00 GMT.</li>
// </ul>
// <P>
// This property and the "number" and "text" strategies for date/datetime fields are provided
// to allow transparent two-way mapping of legacy date values that are stored in numeric or
// text columns respectively rather than a true "date" column.  You would not ordinarily use them
// in the absence of a legacy database.
// <P>
// Note that this property only applies to users of SmartClient server and data sources of
// type "sql".
//
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.sqlTrueValue (String : null : IR)
// For fields of type "boolean" that specify a
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "singleChar", this is the
// value to persist for true values.  Note that the common use cases of storing booleans as
// T/F, Y/N and 1/0 are already catered for without the need for this property.<br>
// For fields of type "boolean" that specify a
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "string", this is the value
// to persist for true values that require using the entire value.
//
// @see dataSourceField.sqlStorageStrategy
// @see dataSourceField.sqlFalseValue
// @serverDS only
// @visibility customSQL
//<

//> @attr dataSourceField.sqlFalseValue (String : null : IR)
// For fields of type "boolean" that specify a
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "singleChar", this is the
// value to persist for false values.  Note that the common use cases of storing booleans as
// T/F, Y/N and 1/0 are already catered for without the need for this property.<br>
// For fields of type "boolean" that specify a
// +link{dataSourceField.sqlStorageStrategy,sqlStorageStrategy} of "string", this is the value
// to persist for false values that require using the entire value.
//
// @see dataSourceField.sqlStorageStrategy
// @see dataSourceField.sqlTrueValue
// @serverDS only
// @visibility customSQL
//<


//> @attr dataSourceField.nullReplacementValue (Any : null : IR)
// If +link{DataSource.noNullUpdates} is set, the value to use for this field when a null
// value is assigned to it on an update operation.  This property overrides the DataSource-level
// properties +link{DataSource.nullStringValue,nullStringValue},
// +link{DataSource.nullIntegerValue,nullIntegerValue},
// +link{DataSource.nullFloatValue,nullFloatValue} and
// +link{DataSource.nullDateValue,nullDateValue}, for this field only.
// <p>
// NOTE: If you use this property, you are responsible for ensuring that the replacement value
// is appropriate and valid.
// @see DataSource.noNullUpdates
// @see DataSource.nullStringValue
// @see DataSource.nullIntegerValue
// @see DataSource.nullFloatValue
// @see DataSource.nullDateValue
// @serverDS allowed
// @visibility external
//<



//> @attr   dataSourceField.ignoreTextMatchStyle    (Boolean : null : IRW)
// <b>NOTE: </b>Only applicable to +link{dataSource.clientOnly,clientOnly} DataSources and the
// built-in +link{group:sqlDataSource,SQL}, +link{group:jpaIntegration,JPA} and
// +link{group:hibernateIntegration,Hibernate} DataSources available in Pro, Power and
// Enterprise versions of SmartClient.
// <p>
// Use this flag to inhibit the normal use of +link{type:TextMatchStyle} for this field.  A
// field with this flag set will always be tested for exact equality in generated queries,
// even for filter-style queries where normal behavior would be to use a substring match or
// similar.
// <p>
// Whether or not the exect match is case-sensitive is determined by the DataSource's
// +link{dataSource.ignoreTextMatchStyleCaseSensitive,ignoreTextMatchStyleCaseSensitive}
// setting.
//
// @serverDS allowed
// @visibility external
//<

// ---------------------------------------------------------------------------------------
// client-side only sorting (canSortClientOnly)

//> @attr   dataSourceField.canSortClientOnly    (boolean : false : IRW)
// When true, this field can only be used for sorting if the data is entirely client-side.
//
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.storeWithHash (HashAlgorithm : null : IR)
// If set, causes the field to be securely hashed before saving on an "add" or "update"
// operation.
//
// @serverDS only
// @visibility external
//<

//> @type HashAlgorithm
// Algorithms to use when hashing field values (see +link{DataSourceField.storeWithHash})
//
// @value "MD5"      Message Digest algorithm 5
// @value "SHA"      Secure Hashing Algorithm (SHA-1)
// @visibility external
//<

// ---------------------------------------------------------------------------------------
// File upload and other binary data properties

//> @attr   dataSourceField.maxFileSize    (Integer : null : IRW)
// For fields of a type that are represented by binary data initially uploaded from a file
// (currently "binary" and "imageFile", see +link{type:FieldType}), this sets the maximum
// file size allowed, in bytes.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSourceField.encodeInResponse (boolean : null : IRW)
// When set for a field with binary data, binary data will be delivered as a Base64 encoded
// string.
// <P>
// When <code>encodeInResponse</code> is not set normal responses will not contain values for
// binary fields at all.  Instead, UI components, such as +link{class:TileGrid} issue a second
// binary fetch for binary data when appropriate, and APIs such as +link{dataSource.downloadFile(),downloadFile()} and
// +link{dataSource.viewFile(),viewFile()} can be used to programmatically trigger downloads.
// <p>
// Only applicable to fields of a type that are represented by binary data (currently "binary"
// and "imageFile", see +link{type:FieldType}).
//
// @serverDS only
// @visibility external
//<

// ---------------------------------------------------------------------------------------
// Presentation Mode

//> @attr dataSourceField.fieldTreeId (String : null : IR)
// Value for the +link{tree.idField} used when +link{dataSource.showFieldsAsTree} is set.
//<

//>@attr dataSourceField.fieldTreeParentId (String : null : IR)
// Value for the +link{tree.parentIdField} used when +link{dataSource.showFieldsAsTree} is set.
//<

// Field Types
// --------------------------------------------------------------------------------------------

//> @type FieldType
// The types listed below are built-in types that +link{DataBoundComponent,databound
// components} understand and treat specially (using type-specific form controls, validators,
// formatters, sorting logic, etc).
// <P>
// You can declare custom types via +link{SimpleType,SimpleType.create()}, with settings that
// will influence DataBound components.  You can also create your own subclasses of databound
// components to add further custom, reusable behaviors based on field.type.
// <P>
// <code>field.type</code> can also be the ID of another +link{DataSource}, which allows you to
// model nested structures such as XML documents (in fact, +link{XMLTools.loadXMLSchema()}
// models XML schema in this way).  Nested DataSource declarations affect how XML and JSON data
// is deserialized into JavaScript objects in the
// +link{group:clientDataIntegration,client-side integration} pipeline, so that you can load
// complex XML documents and have them deserialized into a correctly typed nested data
// structure.
// <P>
// Note: to declare related but <i>separate</i> objects, as in an "Account" object that can be
// related to both a "Contact" object and "Order" objects, use
// +link{dataSourceField.foreignKey}, <b>not</b> a nested structure declaration.
//
// @value "text"      Generic text, e.g. <code>"John Doe"</code>. This is the default field type.
//                    Use <code>field.length</code> to set length.
// @value "boolean"   A boolean value, e.g. <code>true</code>
// @value "integer"   A whole number, e.g. <code>123</code>
// @value "float"     A floating point (decimal) number, e.g. <code>1.23</code>
// @value "date"      A logical date, with no time value (such as a holiday or birthday).
//                    Represented on the client as a JavaScript <code>Date</code> object where
//                    time values are ignored.  See +link{group:dateFormatAndStorage} for more
//                    information on date display and serialization formats. See also
//                    +link{class:Date} for SmartClient extensions to
//                    the <code>Date</code> object.
// @value "time"      A time of day, with no date.  Represented on the client as a JavaScript
//                    <code>Date</code> object in UTC/GMT by default (see also
//                    +link{group:dateFormatAndStorage} and the +link{Time} class).
// @value "datetime"  A date and time, accurate to the second. Represented on the client as a
//                    JavaScript <code>Date</code> object. See also
//                    +link{group:dateFormatAndStorage}
//                    and +link{class:Date} for
//                    SmartClient extensions to the <code>Date</code> object.
// @value "enum"      A text value constrained to a set of legal values specified by the
//                    field's +link{DataSourceField.valueMap,valueMap}, as though a
//                    +link{ValidatorType} of "isOneOf" had been declared.
// @value "intEnum"   An enum whose values are numeric.
// @value "sequence"  If you are using the SmartClient SQL datasource connector, a
//                    <code>sequence</code> is a unique, increasing whole number, incremented
//                    whenever a new record is added. Otherwise, <code>sequence</code> behaves
//                    identically to <code>integer</code>. This type is typically used with
//                    <code>field.primaryKey</code> to auto-generate unique primary keys.
//                    See also +link{dataSourceField.sequenceName} and
//                    +link{dataSource.sequenceMode}
// @value "link"      A string representing a well-formed URL.  Some components
//                    will render this as an HTML link (using an anchor tag for example).
// @value "image"     A string representing a well-formed URL that points to an
//                    image.  Some components will render an IMG tag with the value of this field
//                    as the 'src' attribute to render the image.
// @value "binary"    Arbitrary binary data.  When this field type is present, three additional
// fields are automatically generated.  They are: &lt;fieldName&gt;_filename, &lt;fieldName&gt;_filesize, and
// &lt;fieldName&gt;_date_created where &lt;fieldName&gt; is the value of the <code>name</code>
// attribute of this field.  These fields are marked as
// +link{attr:dataSourceField.hidden}<code>:true</code> to suppress their rendering by default.  You
// can show one or more of these fields by specifying the field with a <code>hidden:false</code>
// override in the fields array of the databound component.
// <i>Stream / view file support for custom DataSources</i>: a custom DataSource or +link{DMI}
// must implement the "viewFile" and "downloadFile" operationTypes and return a single Record
// with a byte[] as the field value for the binary field.  For more detail see
// the overview of +link{group:binaryFields,Binary Fields}.
//
// @value "imageFile" Binary data comprising an image.  Causes +link{ViewFileItem} to be used
//                    when the field is displayed in a form, allowing the image to optionally
//                    be displayed inline.
// @value "any"       Fields of this type can contain any data value and have no default
//                    formatting or validation behavior. This is useful as the
//                    +link{SimpleType.inheritsFrom,parent type} for SimpleTypes
//                    where you do not want any of the standard validation or formatting logic
//                    to be inherited from the standard built-in types.
// @value "custom"    Synonymous with "any".
// @value "modifier"  Fields of this type are automatically populated by the SmartClient Server
//                    with the current authenticated userId as part of "add" and "update"
//                    operations.  By default, fields of this type are hidden and not editable;
//                    the server ignores any value that the client sends in a field of this type.
//                    Note that the "authenticated user" can be set explicitly on the server-side
//                    <code>RPCManager</code> using the setUserId() method, or it can come from
//                    the servlet API if you are using its built-in authentication scheme.  See
//                    the server-side Javadocs for <code>RPCManager</code>.
// @value "modifierTimestamp"  Fields of this type are automatically populated by the SmartClient
//                    Server with the current date and time as part of "add" and "update"
//                    operations.  By default, fields of this type are hidden and not editable;
//                    the server ignores any value that the client sends in a field of this type.
// @value "creator"   Fields of this type are automatically populated by the SmartClient Server
//                    with the current authenticated userId as part of "add" operations.
//                    By default, fields of this type are hidden and not editable;
//                    the server ignores any value that the client sends in a field of this type.
//                    The notes about type "modifier" also apply to fields of this type.
// @value "creatorTimestamp"  Fields of this type are automatically populated by the SmartClient
//                    Server with the current date and time as part of an "add" operation (when
//                    the record is first created).  By default, fields of this type are hidden
//                    and not editable; the server ignores any value that the client sends in a
//                    field of this type.
// @value "password"  Same as "text", but causes +link{PasswordItem} to be used by default for
//                    editing (hides typed-in value).
// @value "ntext"     A special field type specifically for use with Unicode data in conjunction
//                    with the Microsoft SQL Server database.  Field type "ntext" implies the
//                    use of +link{DataSourceField.sqlStorageStrategy,sqlStorageStrategy} "ntext";
//                    other than that, this type is identical to "text"
// @value "localeInt" An integer number with locale-based formatting, e.g. <code>12,345,678</code>.
//                    See +link{group:localizedNumberFormatting,Localized Number Formatting}
//                    for more info.
// @value "localeFloat" A float number with locale-based formatting, e.g. <code>12,345.67</code>.
//                    See +link{group:localizedNumberFormatting,Localized Number Formatting}
//                    for more info.
// @value "localeCurrency" A float number with locale-based formatting and using currency
//                    symbol, e.g. <code>$12,345.67</code>.
//                    See +link{group:localizedNumberFormatting,Localized Number Formatting}
//                    for more info.
// @value "phoneNumber" A telephone number.  Uses +link{formItem.browserInputType} "tel" to
//                    hint to the device to restrict input.  On most mobile devices that have
//                    software keyboards, this cause a specialized keyboard to appear which
//                    only allows entry of normal phone numbers.  When displayed read-only,
//                    a "phoneNumber" renders as an HTML link with the "tel:" URL scheme,
//                    which will invoke the native phone dialing interface on most mobile
//                    devices.  In addition, the CSS style "sc_phoneNumber" is applied.
//                    <p>
//                    By default, "phoneNumber" fields do not include validators, however the
//                    following validator definition would limit to digits, dashes and the
//                    "+" character:
//                    xml:
//                    <p>
//                    &lt;validator type="regexp" expression="^(\(?\+?[0-9]*\)?)?[0-9_\- \(\)]*$"
//                        errorMessage="Phone number should be in the correct format e.g. +#(###)###-##-##" /&gt;
//                    <smartclient>
//                        <p>
//                        or directly in javascript:
//                        <p>
// <pre>
// {type:"regexp", expression:"^(\\(?\\+?[0-9]*\\)?)?[0-9_\\- \\(\\)]*$",
//     errorMessage:"Phone number should be in the correct format e.g. +#(###)###-##-##"}
// </pre>
//                    </smartclient>
//                    <smartgwt>
//                        <p>
//                        or directly in smartgwt:
//                        <p>
// <pre>
// RegExpValidator v = new RegExpValidator();
// v.setType(ValidatorType.REGEXP);
// v.setErrorMessage("Phone number should be in the correct format e.g. +#(###)###-##-##");
// v.setExpression("^(\\(?\\+?[0-9]*\\)?)?[0-9_\\- \\(\\)]*$");
// </pre>
//                    </smartgwt>
//                    and adding "#" and "*" to the regular expressions above would allow for
//                    users to enter special keys sometimes used for extension numbers or
//                    pauses
//
//
// @see type:ListGridFieldType
// @see type:FormItemType
// @serverDS allowed
// @visibility external
//<


// Field formatting with format strings
// ---------------------------------------------------------------------------------------

//> @attr dataSourceField.format (FormatString : null : IRW)
// Format string to use when rendering the value in any +link{DataBoundComponent} or when
// exporting via +link{dataSource.exportData()} or +link{listGrid.exportData()} or
// +link{listGrid.exportClientData()}.
// <P>
// Supported for fields of type "date", "time", "datetime", "int", "float" or any derived
// +link{SimpleType}.
// <p>
// To configure a different format for export, use +link{dataSourceField.exportFormat}.
// <p>
// This is a per-field setting; you can alternatively set a default format for all "date",
// "time" or "datetime" fields via
// <smartclient>
// +link{Date.setNormalDatetimeDisplayFormat()} and related methods on +link{Date}.
// </smartclient>
// <smartgwt>
// +link{Date.setNormalDatetimeDisplayFormat()} and related methods on
// +link{Date}.
// </smartgwt>
// See also +link{group:localizedNumberFormatting} for built-in +link{FieldType,FieldTypes}
// that handle localized currency formatting.
// <p>
// Also note, this property takes precedence over any specified
// +link{DataSourceField.dateFormatter,dateFormatter}, but can be overridden on a per-component
// basis by providing a formatter directly on the component, for example, via
// +link{ListGrid.formatCellValue()} or +link{FormItem.formatValue()}.
//
// @see DataSourceField.exportFormat
// @serverDS allowed
// @visibility external
//<

//> @attr dataSourceField.exportFormat (FormatString : null : IRW)
// An optional +link{FormatString} for this field, for use when
// +link{dataBoundComponent.exportData(),exporting} data to spreadsheet formats (XLS and
// OOXML/XLSX), XML, JSON or CSV.   You can use this property to override the normal
// +link{dataSourceField.format,format} of this field, if any, specifically for exports.
// <p>
// Note, for server-driven exports you can specify default formats for date, time and
// datetime fields by specifying properties <code>export.format.default.date</code>,
// <code>export.format.default.time</code> and <code>export.format.default.datetime</code>
// in your <code>server.properties</code> file.  These formats will be used for fields
// that do not have a "format" or "exportFormat" property specified in the
// <code>.ds.xml</code> file.
// <p>
// Specifically when exporting to spreadsheet formats, the <code>FormatString</code> is
// translated to the type of format string used by spreadsheet programs like Excel.  A handful
// of features are not present in Excel format strings, and some features behave slightly
// differently.  These differences are explained below.
// <p>
// <h3>Excel cannot handle dates prior to January 1st 1900</h3>
// This is a well-known limitation of Excel dates; it is not a formatting issue as such.
// <p>
// <h3>Currency symbols become fixed to the current locale at export time</h3>
// The placeholder currency symbol "&#x00A4" (\u00A4) in a numeric
// format string is rendered as the
// <smartclient>
// +link{NumberUtil.currencySymbol, localized currency symbol}.
// </smartclient>
// <smartgwt>
// localized currency symbol returned by GWT's built-in NumberFormat class.
// </smartgwt>
// When exporting, the format string provided to Excel contains the currency symbol for the
// current locale of the SmartClient application, and not a placeholder currency symbol that
// would make Excel pick up a currency symbol based on the operating system locale.  We do this
// to ensure that the spreadsheet reflects the <em>application's</em> localization, rather than
// the localization of the current user's computer, because they may be different.
// <p>
// <h3>Rounding differences</h3>
// The approach to rounding a positive number to a set number of decimal places is fairly
// universally agreed upon for non-specialized requirements: add 0.5 (or 0.05, or 0.005, or
// whatever) to the number and then truncate.  This leads to the well understood convention
// that numbers exactly halfway between two possible rounding outcomes, go to the larger one.
// So 7.5 becomes 8 and 7.15 becomes 7.2.
// <p>
// However, there is no such universal agreement when it come to rounding negative numbers.
// Some take the view that you should round by taking the number to the larger absolute value,
// so -7.15 becomes -7.2.  Others feel that you should round by taking the number to the larger
// value in the sense of it being "less negative", so -7.15 becomes -7.1.
// <p>
// SmartClient formatting takes the first approach and rounds negative numbers away from zero.
// We do this simply because that is what Java DecimalFormat does.  Unfortunately, Excel does
// the opposite.  Therefore, you will see rounding differences on negative numbers on exact
// 50% boundaries: SmartClient will format -7.15 as -7.2, while Excel will format the same
// value as -7.1.
// <p>
// <h3>Different treatment of '#'</h3>
// Both SmartClient and Excel use '#' to mean "digit, do not show zeroes".  However, Excel
// does not implement this strictly in the integer part when the number it is formatting has
// a value of 0 in the integer part.  So, with a format of "#.##", the value 0.25 is formatted
// as "0.25".  SmartClient (like Java DecimalFormat) is strict: with that format, 0.25 is
// formatted as ".25"; if you want to match Excel's output, you must use the format "0.##".
// <p>
// <h3>Miscellaneous edge cases</h3>
// There is quite a lot of variation in behavior between Java DecimalFormat and Excel around
// the edges.  For actual error cases - for example, a format string that just contains
// nonsense - it is normal and expected that the SmartClient behavior and the Excel behavior
// do not match - this is just two systems producing different garbage out for the same
// garbage in, which is to be expected.  For valid but weird usages - for example, a format
// with multiple percent signs - SmartClient's formatting is in line with what DecimalFormat
// does, unless DecimalFormat throws an Exception, in which case we just do the thing that
// seems most sensible, or the thing that was easiest to implement.
// <p>
// Known differences in behavior in edge cases include:<ul>
// <li>SmartClient ignores formatting characters in the number part of the format string,
// whereas Excel rejects the format (this behavior may vary with different releases of Excel
// and supposedly compatible libraries: POI, for example, will accept such format strings).
// If you attempt to format 5.175 with the format string "#b0.#a#", SmartClient will return
// "5.72", and Excel will reject the format</li>
// <li>SmartClient ignores quoted characters in the number part of the format string,
// whereas Excel preserves them.  If you attempt to format 5.175 with the format string
// "#'b'0.#'a'#", SmartClient will return "5.72", and Excel will return "b5.7a2"</li>
// <li>If you specify the empty string as a format string, SmartClient returns the result of
// calling toString() on the number; Excel uses the built-in "General" format.  These two
// approaches will generally give the same or very similar results, but that is really a
// coincidence: the correct way to obtain matching results in the browser and the spreadsheet
// is to specify a valid format string</li>
// <li>If you specify a format string that contains no number part (ie, no '#' or '0'
// characters), SmartClient does what DecimalFormat does, which is to output the integer part
// of the number alongside the fixed part of the format.  Excel just outputs the fixed part.
// So, if you attempt to format -5.3 with the format string "'Hello world'", SmartClient will
// output "-Hello world5", whereas Excel will output just "Hello world"</li>
// <li>If you specify multiple percent signs in the format, SmartClient treats all but the
// first one as static text, so 0.5 formatted with "#%%%" is "50%%%" (ie, all the signs are
// preserved but there is only one multiplcation by 100).  Excel multiplies for  each percent
// sign, so 0.5 formatted with "#%%%" is "500000%%%"</li>
// </ul>
// <p>
// <h3>Date format functionality not supported by Excel</h3>
// The following date/time/datetime functionality is not supported by Excel; if you use
// formatters that use any of this functionality, your values will be exported to Excel
// incorrectly formatted.  In cases like this, we recommend that you use a separate
// <code>exportFormat</code>, with the intent of exporting your values in a way that is
// similar to your application formatting (which would be specified with the
// +link{dataSourceField.format,format} property), but within the confines of what Excel
// supports.
// <ul>
// <li>Fiscal year, week and day (LL, LLLL, C, CC, c, cc)</li>
// <li>Week in year (w, ww)</li>
// <li>Day in year (D, DD)</li>
// <li>Day number in week (u)</li>
// <li>Explicit control over whether to use 12- or 24-hour notation.  In Excel, this is
// implied by the presence or absence of the AM/PM designator</li>
// <li>If the user's operating system locale is different to the locale in use in the
// SmartClient application, day and month names may be different in the spreadsheet</li>
// </ul>
// <h3>Number format functionality not supported by Excel</h3>
// The only SmartClient number-formatting functionality not supported for
// export to Excel is "multiply by 1000 and show as per mille".
// <p>
// <h3>Limit on number of custom Excel formats</h3>
// Excel limits the number of custom format strings in a single spreadsheet to somewhere
// between 200 and 250, depending on your locale and language.  Hitting this limit in an export
// would require hundreds of field definitions, each defining unique
// <code>FormatStrings</code>.  If you do hit the limit, the only workaround is to use fewer
// unique <code>FormatStrings</code>.
//
// @see DataSourceField.format
// @serverDS allowed
// @visibility external
//<

//> @type FormatString
// A String to be used as a format specifier for a date, datetime, time or numeric field, via
// the +link{dataSourceField.format,format} and +link{dataSourceField.exportFormat,exportFormat}
// properties.
// <p>
// For fields with a numeric type, the format string is similar to java.text.NumberFormat (see
// +externalLink{http://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html,DecimalFormat JavaDoc}),
// and for date, time, and datetime types, the format string is similar to
// java.text.SimpleDateFormat (see
// +externalLink{http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html,SimpleDateFormat JavaDoc}).
// <p>
// Note that all the client-side formatting described in this section is is done by the
// +link{NumberUtil.format(),NumberUtil.format()} and
// <smartclient>+link{DateUtil.format(),DateUtil.format()}</smartclient>
// <smartgwt>{@link com.smartgwt.client.util.DateUtil.format()}</smartgwt>
// methods.  These are static utility methods that your own code can call if you need to
// programmatically format a date or number using a <code>FormatString</code>
// <p>
// There are 3 possible contexts where a <code>FormatString</code> may be interpreted, and each
// has slightly different limitations:
//
// <h4>in-browser rendering &amp; client-driven exports</h4>
// Almost complete support for Java's SimpleDateFormat/DecimalFormat, as described below, with
// some small extensions for formatting with awareness of a +link{FiscalCalendar}.
// <p>
// This category includes cases where code running in the browser does the rendering and
// the rendered result is passed to the server, such as
// +link{ListGrid.exportClientData(),client-driven export} and
// +link{rpcManager.exportContent(),PDF export of the printed view}.
//
// <h4>Excel export</h4>
// Almost the same as in-browser rendering, with minor limitations due to missing features in
// Excel.  Exact differences are described under +link{dataSourceField.exportFormat}.
//
// <h4>non-Excel server export</h4>
// For example, CSV, XML or JSON +link{type:ExportFormat,export formats} provided via
// +link{DataSource.exportData()}.  Full support for SimpleDateFormat/DecimalFormat
// as provided by whichever Java version you have installed on the server.  However note that
// depending on the context of the export, the default behavior may be to ignore format
// strings, since formatting intended for end users wouldn't be desirable if data exchange is
// the goal.  See the +link{group:exportFormatting,Export Formatting overview} for details.
//
// <P>
// <h3>Date Format</h3>
// <table width="700" style="font-size:11px;border:1px solid black;">
// <tr><td style="color:white;background-color:black;"><b>Format token</b></td>
//     <td style="color:white;background-color:black;"><b>Description</b></td>
//     <td style="color:white;background-color:black;width:150px;"><b>Sample value</b></td></tr>
// <tr><td>yy     </td><td>Year as a two-digit number                            </td><td>"99"   or "07"</td></tr>
// <tr><td>yyyy   </td><td>Year as a four-digit number                           </td><td>"1999" or "2007"</td></tr>
// <tr><td>YY     </td><td>Week year as a two-digit number (week year is the year associated with the entire week deemed to contain a given date, and it may differ from calendar year.  For example, the week year of December 31 2012 is 2013)</td><td>"99"   or "07"</td></tr>
// <tr><td>YYYY   </td><td>Week year as a four-digit number                      </td><td>"1999" or "2007"</td></tr>
// <tr><td>LL     </td><td>Fiscal year as a two-digit number (+link{FiscalCalendar})</td><td>"99"   or "07"</td></tr>
// <tr><td>LLLL   </td><td>Fiscal year as a four-digit number                    </td><td>"1999" or "2007"</td></tr>
// <tr><td>M      </td><td>Month in year                                         </td><td>"1"  to "12"</td></tr>
// <tr><td>MM     </td><td>Month in year with leading zero if required           </td><td>"01" to "12"</td></tr>
// <tr><td>MMM    </td><td>Short month name (<smartclient>+link{Date.shortMonthNames}</smartclient><smartgwt>{@link com.smartgwt.client.util.DateUtil#setShortMonthNames()}</smartgwt>)        </td><td>"Jan" to "Dec"</td></tr>
// <tr><td>MMMM   </td><td>Full month name (<smartclient>+link{Date.monthNames}</smartclient><smartgwt>{@link com.smartgwt.client.util.DateUtil#setMonthNames()}</smartgwt>)              </td><td>"January" to "December"</td></tr>
// <tr><td>w      </td><td>Week in year                                          </td><td>"1"  to "52"</td></tr>
// <tr><td>ww     </td><td>Week in year with leading zero if required            </td><td>"01" to "52"</td></tr>
// <tr><td>C      </td><td>Week in fiscal year (+link{FiscalCalendar})           </td><td>"7"  or "29"</td></tr>
// <tr><td>CC     </td><td>Week in fiscal year with leading zero if required     </td><td>"07" or "29"</td></tr>
// <tr><td>d      </td><td>Day of the month                                      </td><td>"1"  to "31"</td></tr>
// <tr><td>dd     </td><td>Day of the month with leading zero if required        </td><td>"01" to "31"</td></tr>
// <tr><td>ddd    </td><td>Short day name (+link{Date.shortDayNames})            </td><td>"Mon" to "Sun"</td></tr>
// <tr><td>dddd   </td><td>Full day name. (+link{Date.dayNames})                 </td><td>"Monday" to "Sunday"</td></tr>
// <tr><td>E      </td><td>Short day name ("EE" and "EEE" are equivalent; all are exactly the same as "ddd" - "E" is supported purely to conform with SimpleDateFormat)</td><td>"Mon" to "Sun"</td></tr>
// <tr><td>EEEE   </td><td>Full day name (exactly the same as "dddd")            </td><td>"Monday" to "Sunday"</td></tr>
// <tr><td>D      </td><td>Day in year                                           </td><td>"1"  to "366"</td></tr>
// <tr><td>DD     </td><td>Day in year with leading zero if required             </td><td>"01" to "366"</td></tr>
// <tr><td>c      </td><td>Day in fiscal year (+link{FiscalCalendar})            </td><td>"5"  or "204"</td></tr>
// <tr><td>cc     </td><td>Day in fiscal year with leading zero if required      </td><td>"05" or "204"</td></tr>
// <tr><td>u      </td><td>Day number in week (1 is Monday)                      </td><td>"1" to "7"</td></tr>
// <tr><td>H      </td><td>Hour in day, 0-23 (24-hour clock)                     </td><td>"0"  to "23"</td></tr>
// <tr><td>HH     </td><td>Hour in day with leading zero if required (24-hour)   </td><td>"00" to "23"</td></tr>
// <tr><td>h      </td><td>Hour in day, 1-12 (12-hour clock)                     </td><td>"1"  to "12"</td></tr>
// <tr><td>hh     </td><td>Hour in day with leading zero if required (12-hour)   </td><td>"01" to "12"</td></tr>
// <tr><td>m      </td><td>Minute in hour                                        </td><td>"0"  to "59"</td></tr>
// <tr><td>mm     </td><td>Minute in hour with leading zero if required          </td><td>"00" to "59"</td></tr>
// <tr><td>s      </td><td>Second in minute                                      </td><td>"0"  to "59"</td></tr>
// <tr><td>ss     </td><td>Second in minute with leading zero if required        </td><td>"00" to "59"</td></tr>
// <tr><td>a      </td><td>The AM/PM designator (+link{Time.AMIndicator})        </td><td>" am" or " pm"</td></tr>
// </table>
// <p>
// <h3>Examples - various formatted versions of the datetime "2006-08-03 11:26:18"</h3>
// <table>
// <tr><td width="150">"M/d/yy"</td><td width="300">8/3/06</td></tr>
// <tr><td>"MMMM yyyy"</td><td>August 2006</td></tr>
// <tr><td>"HH:mm"</td><td>11:26</td></tr>
// <tr><td>"d MMM yyyy, H:ma"</td><td>3 Aug 2006, 11:26 am</td></tr>
// <tr><td>"dd/mm/yyyy"</td><td>03/08/2006</td></tr>
// <tr><td>"CC/LLLL"</td><td>53/2006 (assuming the fiscal year ends in the first week of August)</td></tr>
// </table>
// <p>
// <h3><code>SimpleDateFormat</code> specifiers that we do <b>not</b> support:</h3>
// <ul>
// <li>Era designator, BC/AD (G)</li>
// <li>Day of week in month (F)</li>
// <li>Hour in day, 24-hour, with 1-based instead of normal 0-based numbering (hours are 1-24) (k)</li>
// <li>Hour in day, 12-hour, with 0-based instead of normal 1-based numbering (hours are 0-11) (K)</li>
// <li>Milliseconds (S)</li>
// <li>Timezone (z, Z, X)</li>
// </ul>
// <p>&nbsp;<p>
// <h3>Number Format</h3>
// <table width="700" style="font-size:11px;border:1px solid black;">
// <tr><td style="color:white;background-color:black;"><b>Format char</b></td>
//     <td style="color:white;background-color:black;"><b>Description</b></td>
// <tr><td>0          </td><td>Digit, zero is displayed</td></tr>
// <tr><td>#          </td><td>Digit, zero is not displayed</td></tr>
// <tr><td>-          </td><td>Minus sign</td></tr>
// <tr><td>.          </td><td>Decimal separator</td></tr>
// <tr><td>%          </td><td>Multiply by 100 and show as percentage</td></tr>
// <tr><td>&#x2030; (\u2030) </td><td>Multiply by 1000 and show as per mille.  See below.</td></tr>
// <tr><td>,          </td><td>Indicates digit grouping should be used - eg 1,000,000.  See below.</td></tr>
// <tr><td>;          </td><td>Separates positive and negative subpatterns.  See below.</td></tr>
// <tr><td>&#x00a4; <smartclient>(\u00A4)</smartclient><smartgwt>(\\u00A4)</smartgwt> </td><td>As a prefix or suffix, indicates the local currency symbol should be used.  Note that you must use special notation to include this character in an XML file (such as a .ds.xml file).  See below.</td></tr>
// <tr><td>'          </td><td>Used to quote special characters in a prefix or suffix, for example, "'#'#" formats 123 to "#123". To create a single quote itself, use two in a row: "# o''clock".</td></tr>
// </table>
// <p>
// All other characters in the format string are taken literally and output unchanged.
// <p>
// <h3>Examples</h3>
// <table style="font-size:11px;border:1px solid black;">
// <tr><td width="150" style="color:white;background-color:black;"><b>Format string</b></td>
//     <td width="150" style="color:white;background-color:black;"><b>Zero value</b></td>
//     <td width="150" style="color:white;background-color:black;"><b>Positive value: 12345.678</b></td>
//     <td width="150" style="color:white;background-color:black;"><b>Negative value: -2345.123</b></td>
// <tr><td>"0.00"</td><td>0.00</td><td>12345.68</td><td>-2345.12</td></tr>
// <tr><td>",##0.00"</td><td>0.00</td><td>12,345.68</td><td>-2,345.12</td></tr>
// <tr><td>"0.###"</td><td>0</td><td>12345.678</td><td>-2345.123</td></tr>
// <tr><td>"&#x00a4;,0.00"</td><td>$0.00</td><td>$12,345.68</td><td>-$2345.12</td></tr>
// <tr><td>"0.0#%"</td><td>0.0%</td><td>1234567.8%</td><td>-234512.3%</td></tr>
// <tr><td>"0.0#&#x2030;"</td><td>0.0&#x2030;</td><td>12345678.0&#x2030;</td><td>-2345123.0&#x2030;</td></tr>
// <tr><td>"0.0#'%'"</td><td>0.0%</td><td>12345.68%</td><td>-2345.12%</td></tr>
// <tr><td>"0.00;(0.00)"</td><td>0.0%</td><td>12345.68</td><td>(2345.12)</td></tr>
// </table><br>
// Note, the above examples include cases where there are multiple '#' characters in the integer
// part of the number format (ie, to the left of the decimal separator, or the entire format
// if there is no separator).  We support this pattern simply because <code>DecimalFormat</code>
// does: the extra '#' characters are not significant.  In other words, the format "##0.00"
// produces exactly the same formatting as "0.00", and "##,###,###.##" produces exactly the
// same formatting as ",#.##".  However, multiple '0' characters in the integer part of the
// format <em>are</em> significant, as are both '#' and '0' characters in the decimal part
// of the format (ie, to the right of any decimal separator).
// <p>
// The ";" character marks the boundary between two subpatterns - the first to be used for
// formatting positive numbers (and 0), the second for negative numbers.  Specifying a separate
// pattern for negative numbers is optional: if no negative subpattern is specified, negative
// numbers are formatted like positive numbers, but with a leading "-" sign.
// <p>
// The "&#x00a4;" symbol (\u00A4) is documented in the Java DecimalFormat class as a placeholder for the currency
// symbol appropriate to the current locale.  For client-driven exports, we replace it with
// <smartclient>
// the +link{NumberUtil.currencySymbol, localized currency symbol}.
// Likewise, we use +link{NumberUtil.decimalSymbol,decimalSymbol} and
// +link{NumberUtil.groupingSymbol,groupingSymbol} to localize the formatting of numbers.
// </smartclient>
// <smartgwt>
// whatever GWT's built-in NumberFormat's class uses.  Likewise, the decimal symbol and
// grouping symbol will match what GWT's NumberFormat returns.
// </smartgwt>
// Note that "\u00A4" is the correct way to specify this character in Javascript code.  If you
// need to specify the character in an XML file - the common requirement is in a .ds.xml
// DataSource descriptor file - you must use the code "&amp;#x00A4;" instead.
// <p>
// The "&permil;" per mille symbol is specified as "\u2030" in Javascript code; in XML or HTML
// you can use either the equivalent notation "&amp;#x2030;" or the special HTML code
// "&amp;permil;".
//
// <p>
// <h3><code>DecimalFormat</code> features that we do <b>not</b> support:</h3>
// <ul>
// <li>Scientific notation</li>
// <li>Doubled currency symbol means "use international currency symbol"</li>
// <li>We do not support arbitrary digit grouping, by providing patterns of the '#' and ','
// characters, like <code>DecimalFormat</code> does.  Grouping in SmartClient FormatStrings
// is enabled with a single "," character somewhere before or within the number-formatting
// part of the string - extra "," characters within the number-formatting part of the string
// are tolerated, but they have no effect.  Grouping in SmartClient always causes digits to
// be gathered in groups of three</li>
// </ul>
//
// @see DataSourceField.format
// @see DataSourceField.exportFormat
// @serverDS allowed
// @visibility external
//<


//> @groupDef exportFormatting
// This topic explains the default rules for whether date, numeric and other formatting
// settings are applied when performing various types of exports, and how to override the default
// behavior.
// <p>
// For server-based exports
// (+link{ListGrid.exportData(),ListGrid} or DataSource.exportData()):
// <ul>
// <li> if +link{dsRequest.exportAs,exportAs} is a spreadsheet format (XLS or OOXML),
//      +link{dataSourceField.format,dsField.format or dsField.exportFormat} will be used if
//      specified, otherwise, +link{dataSourceField.dateFormatter} will be used if specified,
//      otherwise, no formatting will be applied and the date or number will be shown in the
//      spreadsheet program's default formatting.
// <li> if targetting CSV, XML or JSON, by default, formatting declarations are ignored and
//      standard formats are used, because the expectation is that this type of export is
//      intended for data interchange with other systems and not for direct viewing by end
//      users.  Specifically, date and datetime values use standard
//      +externalLink{http://www.w3.org/TR/xmlschema-2/#dateTime,XML Schema date and time formats}
//      and CSV export uses the "yyyy-MM-dd HH:mm:ss" expected by Microsoft Excel and similar
//      tools that consume CSV, with only the date or time part of the format being used for
//      fields that are of type "date" or "time" rather than "datetime".  If you instead set
//      +link{dsRequest.exportRawValues} to false, format settings available to server will be used,
//      exactly as explained above for spreadsheet exports with <code>exportData()</code>.
// </ul>
// For a client-driven export (eg +link{listGrid.exportClientData()}):
// <ul>
// <li> if +link{dsRequest.exportAs,exportAs} is a spreadsheet format (XLS or OOXML),
//      rules are the same as for server-driven export except that <code>dateFormatter</code>
//      settings on client-side UI components will be used if a built-in formatter is used (for
//      example, if +link{listGridField.dateFormatter} is set to the built-in formatter
//      "toEuropeanShortDate")).  If you need date values to appear <b>exactly</b> as shown to
//      the user, set +link{dsRequest.exportDatesAsFormattedString}, but see the docs for this property for the
//      drawbacks of doing this.
// <li> for CSV, XML or JSON exports, whatever is shown to the end user is used (since it's
//      assumed the reason for calling <code>exportClientData()</code> rather than
//      <code>exportData()</code> is precisely to create a fully formatted export).  If you
//      instead set +link{listGrid.exportRawValues} to true, only standard formats appropriate
//      to data interchange are used, the same as described for <code>exportData()</code> above.
// </ul>
// @title Exports &amp; Formatting
// @visibility external
//
//<

//> @attr dsRequest.exportRawValues (Boolean : null : IRW)
// Whether formatting settings should be applied to data being exported.  Default behavior and
// the effect of setting of <code>exportRawValues</code> is described in the
// +link{group:exportFormatting,Export Formatting overview}.
// @group exportFormatting
// @visibility external
//<





// Titles
// ---------------------------------------------------------------------------------------

//> @attr DataSource.title (string : dataSource.ID : IRW)
// User-visible name for this DataSource.
// <P>
// For example, for the supplyItem DataSource, "Supply Item".
// <P>
// If is unset, <code>getAutoTitle()</code> method will be used with <code>dataSource.ID</code>.
// value in order to derive a default value for the title.
// <P>
// For example "employee" ID will be derived to "Employee", "team_member" ID will be
// derived to "Team Member".
//
// @group titles
// @serverDS allowed
// @visibility external
//<

//> @attr DataSource.pluralTitle (string : dataSource.ID : IR)
// User-visible plural name for this DataSource.
// <P>
// For example, for the supplyItem DataSource, "Supply Items".
// <P>
// Defaults to <code>dataSource.title</code> + "s".
// @group titles
// @serverDS allowed
// @visibility external
//<

//> @groupDef dsSpecialFields
// A DataSource allows you to declare that certain fields are the most important fields to
// show to a user in situations where space is limited, and only one or a few fields can be
// reasonably shown.
// <P>
// In the table below these special fields are summarized, along with their meaning, and
// examples of which field would be most appropriate from several example DataSources.
// <P>
// <table cellPadding=2 class="normal" border=1>
// <tr style="font-weight:bold;">
//  <td rowSpan="2">Attribute</td>
//  <td colSpan="3">Example DataSource field</td>
//  <td rowSpan="2">Meaning</td>
// </tr><tr style="font-style:italic;">
//  <td>employee</td><td>emailMessage</td><td>stockItem</td>
// </tr><tr>
//  <td>titleField</td><td>name</td><td>subject</td><td>itemName</td>
//      <td>primary label for the record as a while</td>
// </tr><tr>
//  <td>infoField</td><td>job</td><td>sender</td><td>category</td>
//      <td>second most pertinent piece of textual information</td>
// </tr><tr>
//  <td>dataField</td><td>salary</td><td>date</td><td>price</td>
//      <td>most pertinent numeric, date or enum (eg status) field</td>
// </tr><tr>
//  <td>descriptionField</td><td>bio</td><td>messageBody</td><td>description</td>
//      <td>descriptive long text field</td>
// </tr><tr>
//  <td>iconField</td><td>photo</td><td>statusIcon</td><td>thumbnail</td>
//      <td> an image or icon to accompany the title field</td>
// </tr></table>
// <P>
// Examples of the use of these fields include the +link{TileGrid}'s default choice of fields,
// and the +link{EventHandler.setDragTracker(),drag tracker} that follows the mouse cursor when
// data is being dragged between grids.
//
// @visibility external
//<


//> @attr dataSource.titleField (string : see below : IR)
// Best field to use for a user-visible title for an individual record from this dataSource.
// <p>
// For example, for a DataSource of employees, a "full name" field would probably most clearly
// label an employee record.
// <p>
// If not explicitly set, titleField looks for fields named "title", "label", "name", and "id"
// in that order.  If a field exists with one of those names, it becomes the titleField.  If not,
// then the first field is designated as the titleField.
//
// @group titles, dsSpecialFields
// @serverDS allowed
// @visibility external
//<

//> @attr dataSource.iconField (string : see below : IR)
// Designates a field of +link{FieldType,type}:"image" as the field to use when rendering a
// record as an image, for example, in a +link{TileGrid}.
// <p>
// For example, for a DataSource of employees, a "photo" field of type "image" should be
// designated as the iconField.
// <p>
// If not explicitly set, iconField looks for fields named "picture", "thumbnail", "icon",
// "image" and "img", in that order, and will use any of these fields as the iconField if it
// exists and has type "image".
// <P>
// To avoid any field being used as the iconField, set iconField to <code>null</code>.
//
// @group dsSpecialFields
// @serverDS allowed
// @visibility external
//<

//> @attr dataSource.infoField (String : null : IR)
// Name of the field that has the second most pertinent piece of textual information in the
// record, for use when a +link{DataBoundComponent} needs to show a short summary of a record.
// <p>
// For example, for a DataSource of employees, a "job title" field would probably be the second
// most pertinent text field aside from the employee's "full name".
// <p>
// Unlike +link{titleField}, infoField is not automatically determined in the absence of an
// explicit setting.
// @group dsSpecialFields
// @serverDS allowed
// @visibility external
//<


//> @attr dataSource.dataField (String : null : IR)
// Name of the field that has the most pertinent numeric, date, or enum value, for use when a
// +link{DataBoundComponent} needs to show a short summary of a record.
// <P>
// For example, for a DataSource of employees, good choices might be the "salary" field, "hire
// date" or "status" (active, vacation, on leave, etc).
// <p>
// Unlike +link{titleField}, dataField is not automatically determined in the absence of an
// explicit setting.
// @group dsSpecialFields
// @serverDS allowed
// @visibility external
//<

//> @attr dataSource.descriptionField (String : null : IR)
// Name of the field that has a long description of the record, or has the primary text data
// value for a record that represents an email message, SMS, log or similar.
// <p>
// For example, for a DataSource representing employees, a field containing the employee's
// "bio" might be a good choice, or for an email message, the message body.
// <p>
// If descriptionField is unset, it defaults to any field named "description" or "desc" in the
// record, or the first long text field (greater than 255 characters) in the record, or null if
// no such field exists.
// @group dsSpecialFields
// @serverDS allowed
// @visibility external
//<

//> @attr dataSource.childrenField (string : null : [IR])
// fieldName for a field in the dataSource expected to contain an explicit array of child nodes.
// Enables loading a databound tree as a hierarchical data structure, rather than a flat list of
// nodes linked by foreignKey.<br>
// Note this is an alternative to setting +link{dataSourceField.childrenProperty} directly on
// the childrenField object.<P>
// By default the children field will be assumed to be +link{dataSourceField.multiple, multiple},
// for XML databinding. This implies that child data should be delivered in the format:
// <pre>
//      &lt;childrenFieldName&gt;
//          &lt;item name="firstChild" ...&gt;
//          &lt;item name="secondChild" ...&gt;
//      &lt;/childrenFieldName&gt;
// </pre>
// However data may also be delivered as a direct list of <code>childrenFieldName</code>
// elements:
// <pre>
//      &lt;childrenFieldName name="firstChild" ...&gt;
//      &lt;childrenFieldName name="secondChild" ...&gt;
// </pre>
// If you want to return your data in this format, you will need to explicitly set
// <code>multiple</code> to false in the appropriate dataSource field definition.
//
// @see dataSourceField.childrenProperty
// @group dataSourceRelations
// @serverDS allowed
// @visibility external
//<

//> @groupDef serverDataSourceImplementation
// NOTE: This section includes references to server-side classes and methods; you can find the
// documentation for these in the server-side Javadocs.
// <p>
// Bear in mind the following points when working with DataSources (whether custom or
// built-in) on the SmartClient Server:<ul>
// <li>There is no requirement or expectation that DataSource subclasses be threadsafe.  As
// long as you use documented techniques for creating DataSource instances, the server framework
// will ensure that no single instance is used concurrently by more than one thread.</li>
// <p>
// <li>If you are using +link{group:springIntegration,Spring-injected dataSources}, note that
// the default scope for a Spring bean is "singleton".  This means that a single instance of
// the bean is cached by Spring and returned every time the framework code asks for that
// DataSource.  This pattern prevents the SmartClient framework from making the above-mentioned
// guarantee that no single instance will be used by multiple threads, and can lead to
// unpleasant concurrency bugs.  The solution is to mark your Spring beans with a scope of
// "prototype" (you could also use "request", but SmartClient caches DataSource instances
// per-HttpServletRequest anyway, so nothing is gained by doing that).  See the
// <a href=http://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/beans.html#beans-factory-scopes>
// Spring documentation</a> for further information about bean scope.
// <p>
// <li>DataSource instances can safely be retained for use multiple times in a given HTTP
// request; this presents no thread-safety issues unless you specifically introduce them by,
// eg, creating a new DSRequest and executing it in its own thread.  However, note that mixing
// +link{DataSource.autoJoinTransactions,transactional} and non-transactional operations on the
// same DataSource instance in a single HTTP request can lead to connection leaks if you are
// using the built-in Hibernate or JPA DataSource.</li>
// <p>
// <li>If the +link{group:server_properties,server.properties} attribute <code>datasources.pool.enabled</code>
// is set, DataSource instances are automatically cached in an in-memory pool.  This caching
// is smart: if the underlying <code>.ds.xml</code> file changes, cached instances are
// discarded and rebuilt.</li>
// <p>
// <li>If pooling is not switched on, DataSource instances are constructed new as required.
// Whether or not pooling is enabled, new DataSource instances are always constructed using
// cached configuration for better performance.  The configuration file (<code>.ds.xml</code>
// file) is read the first time the DataSource is requested; thereafter, we will only re-read
// the configuration file if its last-changed timestamp changes.  This scheme combines the
// performance benefits of caching with the with the convenience of being able to change a
// <code>.ds.xml</code> file and pick up the changes without restarting the server.</li>
// <p>
// <li>The safe way to obtain a DataSource instance is to use
// <code>RPCManager.getDataSource()</code>.  If pooling is enabled, this is the only way to
// obtain a DataSource instance with the guarantee that it will be returned to the pool at
// the end of request processing.</li>
// <p>
// <li>If you are using the <code>DynamicDSGenerator</code> to provide DataSources to the
// framework dynamically:<ul>
// <p>
// <li>You are expected to use one of the <code>DataSource.fromXML()</code> APIs to construct
// your DataSources from an XML document or String of XML text</li>
// <p>
// <li>Do not use normal Java instantiation techniques like <code>new DataSource()</code>, as
// this will introduce thread-safety issues</li>
// <p>
// <li>Return a new DataSource instance each time - returning the same DataSource twice will
// introduce thread-safety issues.  If you are concerned about performance, ensure pooling is
// enabled as described above - this will mean that the DataSource you return will be
// automatically cached and re-used, so your DynamicDSGenerator will only be called enough
// times to populate the pool.  If you are still concerned about performance, cache the XML
// String that you pass to <code>DataSource.fromXML()</code>.</li>
// <p>
// <li>For many applications of <code>DynamicDSGenerator</code>, pooling is inappropriate
// because the returned DataSource for a given name might be different each time the generator
// is called.  For this reason, pooling of dynamic DataSources is disabled by default.  To
// enable pooling for dynamically-generated DataSources, set the +link{group:server_properties,server.properties}
// flag <code>poolDynamicDataSources</code> to true.  NOTE: Here, "dynamic DataSource" means
// a DataSource whose name would cause the framework to invoke a DynamicDSGenerator, which
// doesn't necessarily mean that the generator would actually create a dynamic DataSource.  As
// the server-side documentation for <code>DynamicDSGenerator.getDataSource()</code> states,
// a generator can simply return null to decline the opportunity to create a dynamic DataSource.
// Therefore, if the proper operation of pooling is important to you, avoid patterns of
// <code>DynamicDSGenerator</code> usage that involve registering more generically than you
// need to.  In particular, avoid the <code>addDynamicDSGenerator()</code> signature that
// does not take a prefix or regex parameter - this will cause the pooling subsystem to regard
// <b>all</b> DataSources as dynamic, and will effectively disable all DataSource pooling.
// </li></ul>
// </li></ul>
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title Notes on Server-side DataSource Implementations
// @visibility external
//<

//> @attr dataSource.serverConstructor (String : null : IR)
//
// This property allows you to write and use custom DataSource subclasses on the server, by
// specifying either <ul>
// <li>the fully-qualified name of the DataSource subclass that should be instantiated
// server-side for this dataSource, or</li>
// <li>the token "spring:" followed by a valid Spring bean ID, if you wish to instantiate
// your custom dataSource object using Spring dependency injection.  For example,
// <code>"spring:MyDataSourceBean"</code>.  See also +link{group:serverInit} for special
// concerns with framework initialization when using Spring.  It is also particularly important
// that you read the discussion of caching and thread-safety linked to below, as there are
// special considerations in this area when using Spring.</li>
// </ul>
// <p>
// One reason you might wish to do this would be to override the validate() method to provide
// some arbitrary custom validation (such as complex database lookups, validation embedded in
// legacy applications, etc).  It is also possible - though obviously a more substantial task -
// to override the execute() method in your custom DataSource.  This is one way of creating
// a completely customized DataSource implementation.
// <p>
// <b>Note:</b> If you use this property, you are responsible for making sure that it refers to
// a valid server-side class that extends <code>com.isomorphic.datasource.BasicDataSource</code>,
// or to a Spring bean of the same description.
// If your implementation relies on methods or state only present in certain specialized
// subclasses of DataSource (for example, you want the normal behavior and features of a
// HibernateDataSource, but with a specialized validate() method), then you should extend the
// subclass rather than the base class.
// <P>
// NOTE: Please take note of the points made in
// +link{group:serverDataSourceImplementation,this discussion} of caching and thread-safety
// issues in server-side DataSources.
//
// @serverDS only
// @visibility external
//<

//> @attr dataSource.qualifyColumnNames (Boolean : true : IR)
//
// For dataSources of +link{serverType,serverType} "sql", determines whether we qualify column
// names with table names in any SQL we generate.  This property can be overridden on specific
// operationBindings.
//
// @see OperationBinding.qualifyColumnNames
// @serverDS allowed
// @visibility external
//<

//> @attr dataSource.validateRelatedRecords (boolean : null : IR)
//
// If true, indicates that the SmartClient Server should automatically apply a
// +link{ValidatorType} of "hasRelatedRecord" to every field
// on this dataSource that has a +link{DataSourceField.foreignKey,foreignKey} defined.
//
// @visibility external
//<

//> @attr dataSource.beanClassName (String : null : IR)
//
// This property has different meanings depending on the +link{dataSource.serverType,serverType}:
// <p>
// <b>For SQL DataSources (DataSources with serverType "sql")</b><br>
// If set, results from the database will be used to create one instance of the indicated Java
// bean per database row.  Otherwise a Map is used to represent each row retrieved from SQL.
// <P>
// With this feature active, a DSResponse from this DataSource will contain a Collection of
// instances of the indicated <code>beanClassName</code>, available via DSResponse.getData().
// This creates a couple of possibilities:
// <P>
// <dl>
// <dt>Add business logic for derived properties, such as computed formulas</dt>
// <dd>For example, declare a DataSourceField named "revenueProjection".  By default this
// field will call getRevenueProjection() on your bean to retrieve the value to send to the
// client.  Your implementation of getRevenueProjection() could apply some kind of formula to
// other values loaded from the database.</dd>
// <dt>Call business logic on retrieved beans via DMI</dt>
// <dd>By adding a +link{group:dmiOverview,DMI} method that calls DSRequest.execute() to retrieve a DSResponse,
// you have an opportunity to call business logic methods on the beans representing each
// row affected by the DSRequest.  For example, notify a related BPEL process of changes to
// certain fields.</dd>
// </dl>
// <P>
// By using <code>beanClassName</code> on a specific +link{operationBinding}, you can:
// <ul>
// <li>Use a bean to represent your data only when it matters; for example, avoid the overhead
//     of using a bean for "fetch" operations, but do use a bean for "update" operations so
//     that you can execute relevant business logic after the update completes.</li>
// <li>Skip the use of beans for complex reporting queries that produce results unrelated to
//     your persistent object model.  Set beanClassName to blank ("") on a specific
//     operationBinding to override DataSource.beanClassName for that specific operation.</li>
// <li>For SQL joins that produce additional data fields, use a special, operation-specific
//     bean that represents a join of multiple entities and contains business logic specific
//     to that joined dataset</li>
// </ul>
// <P>
// Note that setting <code>beanClassName</code> has no effect on the DSRequest.  However,
// +link{group:dmiOverview,DMI} has a built-in facility for populating a bean with the inbound
// +link{DSRequest.data} - just declare the bean as a method argument.
// <p>
// <b>For generic DataSources (DataSources with serverType "generic")</b><br>
// +link{group:visualBuilder,Visual Builder} sets this property when it creates a generic
// DataSource using the Javabean Wizard.  It has no built-in server-side effects.
// <p>
// <b>For Hibernate DataSources (DataSources with serverType "hibernate")</b><br>
// The name of the Java bean or POJO class that is mapped in Hibernate.  This will typically
// be the fully-qualified class name - eg <code>com.foo.MyClass</code> - but it may be the
// simple class name - just <code>MyClass</code> - or it may be some other value.  It all
// depends on how your classes have been mapped in Hibernate.
// <p>
// Note:  If you are intending to use Hibernate as a data-access layer only,
// you do not need to create Hibernate mappings or Java objects: SmartClient will generate
// everything it needs on the fly.
// <p>
// <b>For JPA DataSources (DataSources with serverType "jpa" or "jpa1")</b><br>
// The fully qualified class name of the JPA annotated entity.
// <p>
// <b>NOTE for Hibernate and JPA users:</b> When you use JPA, or use Hibernate as a full ORM
// system (ie, not just allowing SmartClient Server to drive Hibernate as a data access layer),
// the beans returned on the server-side are <b>live</b>.  This means that if you make any
// changes to them, the ORM system will persist those changes.  This is true even if the beans
// were created as part of a fetch operation.
// <p>
// This causes a problem in the common case where you want to use a DMI or custom DataSource
// implementation to apply some post-processing to the beans fetched from the persistent
// store.  If you change the values in the beans directly, those changes will be persisted.
// <p>
// If you want to alter the data returned from a JPA or Hibernate persistent store as part of
// a fetch request just so you can alter what gets sent to the client, you can use the
// server-side <code>DSResponse</code>'s <code>getRecords()</code> method.  This will return
// your bean data in "record" format - ie, as a List of Maps.  You can alter these records
// without affecting your persistent store, and then call <code>setData()</code> on the
// <code>DSResponse</code>), passing the altered list of records.  See the server-side Javadocs
// for <code>DSResponse</code> for details of these two methods.
//
// @see OperationBinding.beanClassName
// @serverDS only
// @visibility external
//<

isc.DataSource.addMethods({
    init : function () {
        // serverType:"sql" implies iscServer format
        if (this.serverType == "sql") this.dataFormat = "iscServer";

        // make it unnecessary to set dataFormat="xml" when properties related to XML have
        // already been set
        if (this.dataFormat == "iscServer" &&
            (this.serviceNamespace != null || this.recordXPath != null)) this.dataFormat = "xml";

        // we don't currently support request queuing (multi-op) for anything other than the
        // iscServer format
        this.canQueueRequests = (this.dataFormat == "iscServer" || this.clientOnly);

        // if testData is set and cacheData isn't, set cacheData now, since testData is marked
        // as deprecated
        if (this.testData && !this.cacheData) this.cacheData = this.testData;
        // if cacheData is set and testData isn't, and we're in clientOnly mode, set testData
        // now, since internal code requires it but it's marked externally as deprecated
        else if (this.clientOnly) {
                this.cacheData = this.testData = this.cacheData || this.testData;
        }


        if (this.ID == null && this.id != null) this.ID = this.id;


        if (this.name == null) this.name = this.ID;

        // prevent the server definition of eg ValueMap from destroying certain built-in schema
        var existingDS = isc.DS.get(this.ID);
        if (existingDS && existingDS.builtinSchema) return existingDS;

        // make the DataSource a global variable if set to do so, and it doesn't belong to a
        // particular namespace
        // NOTE: when becoming global, only clobber other DataSources, never a Class or any
        // other kind of global.  Otherwise widget DataSources like Canvas clobber the
        // corresponding widget class.  Note we have to specially check for isA.ClassObject()
        // because the DataSource classObject and subclasses will return true when
        // isA.DataSource() is applied to them.
        var existingValue = window[this.ID];
        if (this.addGlobalId && this.addGlobalId != isc._false &&
            (!existingValue ||
                (!isc.isA.ClassObject(existingValue) && isc.isA.DataSource(existingValue))))
        {
            isc.ClassFactory.addGlobalID(this);
        }


        // if field is provided as an Array, make them into an index by name (so client-only
        // DataSources can use the array of fields format common to all components).  NOTE:
        // this is essentially Array.makeIndex, except we log collisions.
        var fields = this.fields;
        if (isc.isAn.Array(fields)) {
            if (isc.Canvas.validateFieldNames) {
                // loop through each field and check that each field name is a valid JavaScript identifier.
                isc.Canvas._validateFieldNames(fields, this);
            }

            var fieldIndex = {};
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i];
                // if includeFrom is set, but name isn't pick up name from includeFrom property
                if (field.includeFrom != null) {
                    var split = field.includeFrom.split(".");
                    if (split == null || split.length  != 2) {
                        this.logWarn("Field has includeFrom specified as :"  + field.includeFrom
                            + " format not understood - clearing this property");
                        field.includeFrom = null;
                    } else {
                        if (field.name == null) field.name = split[1];
                    }
                }

                // last field wins on name collision.
                if (fieldIndex[field.name] != null) {
                    this.logWarn("field.name collision: first field: " +
                                 this.echo(fieldIndex[field.name]) +
                                 ", discarded field: " + this.echo(field));
                    continue;
                }
                field = this._prettyField(field);
                if (field.type) {
                    var type = this.getType(field.type);
                    if (type && type.fieldProperties) {
                        field = fields[i] = isc.addProperties({}, type.fieldProperties, field);
                    }
                    if (!field.format && type && type.format) {
                        field.format = type.format;
                    }
                }
                fieldIndex[field.name] = field;
            }
            this.fields = fieldIndex;
        }

        if (this.dataSourceType == isc.DataSource.VIEW) this.initViewSources();

        // register globally
        isc.DataSource.registerDataSource(this);
    },

    _prettyField : function (field) {
        if (isc.isA.String(field.validOperators)) {
            field.validOperators = [field.validOperators];
        }

        var undef;
        if ((field.foreignKey)
            && (field.nillable == undef)) {
            field.nillable = true;
        }

        return field;
    },

    // destroy global ID, if one was assigned.
    destroy : function () {
        var ID = this.ID,
            DS = isc.DS;

        if (this.cacheResultSet) {
            // if there's a cacheResultSet, destroy it now, along with the parent DS
            this.cacheResultSet.destroy();
            this.cacheResultSet = null;
        }

        // remove from global scope
        if (ID && this == window[ID]) window[ID] = null;

        // remove from global registries
        if (DS._dataSources[ID] == this) DS._dataSources[ID] = null;
        if (DS._elements[ID] == this) DS._elements[ID] = null;
        if (DS._types[ID] == this) DS._types[ID] = null;
        this.Super("destroy", arguments);
    },


    // Result Sets
    // --------------------------------------------------------------------------------------------
    getResultSet : function (params) {
        var resultSetClass = isc.ClassFactory.getClass(this.resultSetClass || isc.ResultSet);

        if (!isc.isA.Class(resultSetClass)) {
            this.logWarn("getResultSet(): Unrecognized 'resultSetClass' property:"
                         + resultSetClass + ", returning a standard isc.ResultSet.");
            resultSetClass = isc.ResultSet;
        }

        // flag resultSet as autoCreated, so we know to destroy() it when the component no
        // longer needs it
        return resultSetClass.create(params, {_autoCreated: true});
    },

    // Observable dataChanged methods.
    // ResultSets associated with this dataSource will observe these methods and use them
    // to handle cache-sync
    // Dont obfuscate: allow observers to pick up parameter names
    dataChanged : function (dsResponse, dsRequest) {
//        alert("dataChanged: " + this);
        //!DONTOBFUSCATE
    },



    //> @method DataSource.updateCaches()
    // Causes any components using this DataSource to be notified of changes that have been made to
    // the remote dataset accessed via this DataSource, as though the provided DSResponse had just
    // successfully completed.  This will cause cache managers such as +link{ResultSet} or
    // +link{ResultTree} to automatically update their caches, and components using such cache
    // managers to visually update to show modified data.
    // <P>
    // This API should be used when you have found out about changes made by other users or by
    // automatic processes.  For example, using the SmartClient +link{Messaging} system to receive
    // real-time updates via HTTP streaming, you may get updates that should affect a ListGrid
    // which is using a ResultSet to view a portion of a large dataset.
    // <P>
    // The provided <code>DSResponse</code> should have
    // +link{dsResponse.operationType,operationType} "update", "add" or "remove" - there is no
    // way for a "fetch" response to meaningfully update arbitrary caches.  However, if you
    // have a list of updated records (possibly retrieved via +link{fetchData()}) you can call
    // <code>updateCaches()</code> multiple times with DSResponses of type "update" formed from
    // the list of records retrieved via <code>fetchData()</code>.
    // For example, if you had a ListGrid bound to the <code>supplyItem</code> sample DataSource,
    // and that ListGrid was showing a Record with <code>itemId</code> 23, and you wanted to update
    // the <code>unitCost</code> field to a new value, you would use the following code:<br><br>
    // <smartclient>
    //    <code>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// updatedRecord is the record we want to update<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var record = supplyItemDS.copyRecord(updatedRecord);<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;record.unitCost = 500;<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;var dsResponse = {<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;data: [record],<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;operationType: "update"<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;supplyItemDS.updateCaches(dsResponse);<br>
    //   </code>
    // </smartclient>
    // <smartgwt>
    //    <code>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// updatedRecord is the record we want to update<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Record record = supplyItemDS.copyRecord(updatedRecord);<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;record.setAttribute("unitCost", 500);<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DSResponse dsResponse = new DSResponse();<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dsResponse.setData(record);<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dsResponse.setOperationType(DSOperationType.UPDATE);<br>
    //    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;supplyItemDS.updateCaches(dsResponse);<br>
    //    </code>
    // </smartgwt>
    // <P>
    // To cause all components that have cache managers to drop their caches, provide a
    // DSResponse with +link{dsResponse.invalidateCache} set.
    // <P>
    // As an alternative to calling <code>updateCaches()</code> directly, if updates to other
    // DataSources occur as a result of server-side logic, you can use the server-side API
    // DSResponse.addRelatedUpdate(DSResponse) (Pro Edition and above), which ultimately calls
    // <code>updateCaches()</code> for you - see that method's documentation for details.
    // <P>
    // <b>NOTE:</b>: this API should <b>not</b> be used with a +link{dataSource.clientOnly,clientOnly} DataSource,
    // because in this case, the "remote dataset" is actually within the browser.  Instead,
    // +link{DataSource.updateData()}, addData() or removeData() can be called in order to both
    // change the dataset stored inside the browser and notify all cache managers.
    // <P>
    // If a DataSource has +link{cacheAllData} set and a full cache has been obtained, calling
    // <code>updateCaches</code> will automatically update the cache.
    // <P>
    // Note that the DSResponse provided to this method will <b>not</b> go through
    // +link{transformResponse()} or other processing that would normally occur for a
    // DSResponse resulting from a DSRequest sent by the application in this page.
    //
    // @param dsResponse (DSResponse) the provided DSResponse must minimally have
    //                                dataSource, operationType, and data set
    // @param [dsRequest] (DSRequest) optional dsRequest.  If not specified, a DSRequest will be
    //                                automatically created based on the DataSource and operationType of
    //                                the DSResponse
    // @visibility external
    //<
    updateCaches : function (dsResponse, dsRequest) {
        // If dsRequest is null, generate a dummy request with the appropriate operationType
        // and dataSource
        if (dsRequest == null) {
            dsRequest = {
                operationType:dsResponse.operationType,
                dataSource:this
            };
            if (dsResponse.clientContext != null) {
                dsRequest.clientContext = dsResponse.clientContext;
            }
            if (dsResponse.internalClientContext != null) {
                dsRequest.internalClientContext = dsResponse.internalClientContext;
            }
        } else {
            // ensure the dsRequest is marked with a DataSource.  Downstream code depends on this
            dsRequest.dataSource = dsRequest.dataSource || dsResponse.dataSource || this;
        }

        var updateData = dsResponse.data,
            forceCacheInvalidation = dsResponse.invalidateCache,
            responseCode = dsResponse.httpResponseCode;

        // check the response code for a valid empty file, in which case we just keep
        // the client-side updates.
        if (!updateData && !forceCacheInvalidation
            && !(responseCode >= 200 && responseCode < 300)) {
            //>DEBUG
            this.logWarn("Empty results returned on '" + dsRequest.operationType +
                         "' on dataSource '" + dsRequest.dataSource +
                         "', unable to update resultSet(s) on DataSource " + this.ID +
                         ".  Return affected records to ensure cache consistency.");
            //<DEBUG
            return;
        }

        // if we're caching all data and the cache is full, force refresh if data is invalid


        if (!this.clientOnly && this.cacheAllData && this.hasAllData() &&
            !isc.isAn.Array(updateData))
        {
            this.invalidateCache();
        }

        this.dataChanged(dsResponse, dsRequest);
    },

    // ---------------------------------------------------------------------------------------
    // XML
    // ---------------------------------------------------------------------------------------

    //> @method dataSource.getLegalChildTags()
    // For a DataSource that describes a DOM structure, the list of legal child elements that can
    // be contained by the element described by this DataSource.
    // <p>
    // For a DataSource described by XML schema, this is the list of legal subelements <b>of
    // complexType</b> (elements of simpleType become DataSourceFields with atomic type).
    // <p>
    // Note that currently, if an XML schema file contains ordering constraints, DataSources
    // derived from XML Schema do not capture these constraints.
    //
    // @group xmlSchema
    // @visibility xmlBinding
    //<
    getLegalChildTags : function () {
        var fieldNames = this.getFieldNames(),
            complexFields = [];
        for (var i = 0; i < fieldNames.length; i++) {
            if (this.fieldIsComplexType(fieldNames[i])) complexFields.add(fieldNames[i]);
        }
        return complexFields;
    },

    // get the operationBinding for this operation.  The operationBinding is the bundle of
    // properties specifying how to invoke a particular DataSource operation, eg "fetch".
    // <BR>
    // In the absence of an operationBinding for a particular operation, the DataSource itself
    // is used as the operationBinding, with the same set of properties supported - in this
    // case you basically have a read-only DataSource, only capable of fetching.
    // <BR>
    // Takes either a dsRequest or an operationType and optional operationId.
    getOperationBinding : function (operationType, operationId) {
        if (operationType == null || this.operationBindings == null) return this;

        if (isc.isAn.Object(operationType)) {
            // passed a DSRequest
            var dsRequest = operationType;
            operationType = dsRequest.operationType;
            operationId = dsRequest.operationId;
        }

        var schemaName;
        // look for a binding specific to the operationId (eg myFetchSchema) if passed
        if (operationId) {
            var operationBinding = this.operationBindings.find("operationId", operationId);
            if (operationBinding) return operationBinding;
        }
        // look for a binding for this operationType
        if (operationType) {
            var operationBinding = this.operationBindings.find("operationType", operationType);
            if (operationBinding) return operationBinding;
        }
        return this;
    },

    getDataFormat : function (operationType, operationId) {
        return this.getOperationBinding(operationType, operationId).dataFormat || this.dataFormat;
    },

    // If preventHTTPCaching is set, use the RPCRequest.bypassCache attribute
    shouldBypassCache : function (operationType, operationId) {
        var preventCache = this.getOperationBinding(operationType, operationId).preventHTTPCaching;
        if (preventCache == null) preventCache = this.preventHTTPCaching;
        return preventCache;
    },

    //> @method dataSource.copyRecord()
    // Copies all DataSource field values of a Record (including a TreeNode) to a new
    // Record, omitting component-specific metadata such as selected state from grids,
    // or parent folders for TreeNodes.
    // @param record (Record) The record to be copied.
    // @return  (Record) A new copy of the record provided as an argument, with
    // component-specific metata data removed.
    // @visibility external
    //<
    copyRecord : function (original) {
        if (original == null) return null;

        var copy = {},
            fieldNamesArray = this.getFieldNames(false);

        for (var i = 0; i < fieldNamesArray.length; i++) {

            var fieldName = fieldNamesArray.get(i),
                value = original[fieldName],
                field = this.getField(fieldName);

            if (isc.isA.Date(value)) {
                var dateCopy = new Date();

                dateCopy.setTime(value.getTime());
                dateCopy.logicalDate = value.logicalDate;
                dateCopy.logicalTime = value.logicalTime;

                copy[fieldName] = dateCopy;

            // If the multiple flag is true and the field type is not
            // datasource then we want to duplicate the array if it is one.
            } else if (isc.isAn.Array(value) &&
                       field.multiple == true &&
                       field.type == null)
            {
                var arrayCopy = [];

                for (var j =0; j < value.length; j++){

                    if (isc.isA.Date(value[j])) {
                        var dateCopy = new Date();

                        dateCopy.setTime(value[j].getTime());
                        dateCopy.logicalDate = value[j].logicalDate;
                        dateCopy.logicalTime = value[j].logicalTime;

                        arrayCopy[j] = dateCopy;
                    } else {
                        arrayCopy[j] = value[j];
                    }
                }

                copy[fieldName] = arrayCopy;
            } else {
                copy[fieldName] = value;
            }
        }
        return copy;
    },

    //> @method dataSource.copyRecords()
    // Copies all DataSource field values of an (Array) of Records (including a TreeNode)
    // to a new array of Records, omitting component-specific metadata such as selected state
    // from grids, or parent folders for TreeNodes. This method calls
    // +link{DataSource.copyRecord()} for each item in the array.
    // @param records (Array of Record) The array of Record objects to be copied.
    // @return (Array of Record) A new copy of each record provided in the array argument,
    // with component-specific metata data removed.
    // @visibility external
    //<
    copyRecords : function (originals) {
        if (originals == null) return null;

        var copies = [];

        for (var i = 0; i < originals.length; i++) {
            var original = originals[i],
                copy = this.copyRecord(original);

            copies[i] = copy;
        }
        return copies;
    },

    //> @method dataSource.transformRequest() [A]
    // For a dataSource using +link{group:clientDataIntegration,client-side data integration},
    // return the data that should be sent to the +link{dataURL}.
    // <P>
    // By default, HTTP requests sent to non-SmartClient servers do not include DSRequest
    // metadata such as +link{dsRequest.startRow}, +link{dsRequest.endRow,endRow},
    // and +link{dsRequest.oldValues,oldValues}.  Only the core
    // +link{group:dataSourceOperations,datasource protocol data} is sent, such as the criteria
    // passed to +link{listGrid.fetchData,fetchData()} or the updated values submitted by
    // +link{dynamicForm.saveData(),form.saveData()}.
    // <P>
    // transformRequest() allows you to transform dsRequest metadata into a
    // format understood by your server and include it in the HTTP request, so that you can
    // integrate DataSource features such as data paging with servers that support such
    // features.
    // <P>
    // How the data is actually sent to the URL is controlled by
    // +link{operationBinding.dataProtocol}.  If using the "getParams" or
    // "postParams" protocol, data is expected to be a JavaScript Object where each property
    // will become a GET or POST'd parameter.  If using dataProtocol:"soap" or "postXML", data
    // will be serialized as an XML message by +link{method:DataSource.xmlSerialize()}.
    // <P>
    // As an example, if you have a dataURL that can return paged data given URL parameters
    // "start" and "end", you could implement transformRequest like so:
    // <pre>
    //   isc.DataSource.create({
    //      ...
    //      transformRequest : function (dsRequest) {
    //         if (dsRequest.operationType == "fetch") {
    //             var params = {
    //                start : dsRequest.startRow,
    //                end : dsRequest.endRow
    //             };
    //             // combine paging parameters with criteria
    //             return isc.addProperties({}, dsRequest.data, params);
    //         }
    //      }
    //   });
    // </pre>
    // Other reasons to implement transformRequest():
    // <ul>
    // <li> transform a +link{type:Criteria} object into the custom query language of a web
    //      service
    // <li> add a session id to requests that require authentication
    // <li> detect colliding updates by sending both updated values and the values the user
    //      originally retrieved before editing began (available as +link{dsRequest.oldValues})
    // </ul>
    // <i>Special case:</i> If the <code>dataProtocol</code> is <code>"clientCustom"</code>
    // the SmartClient system will not attempt to send data to the server in any way. Instead
    // transformRequest should be implemented such that it accesses or updates the underlying
    // data-set and calls +link{DataSource.processResponse()} when the operation is complete. This
    // setting allows straightforward integration with non SmartClient comm mechanisms that
    // directly send requests to the server (such as GWT-RPC), or handle data manipulation without
    // sending HTTP at all (such as Google Gears).
    // <P>
    // Note: The +link{RestDataSource} class overrides transformRequest() to handle xml-serializing
    // the request (including meta data) into a standard format.
    //
    // @param dsRequest (DSRequest) the DSRequest being processed
    // @return (any) data to be sent to the dataURL
    // @visibility xmlBinding
    //<
    transformRequest : function (dsRequest) {
        return dsRequest.data;
    },

    //> @method dataSource.getUpdatedData() [A]
    // Helper method to retrieve the updated data from a successful dataSource update or add
    // operation.
    // @param dsRequest (DSRequest) Update request object passed to the server. Note that this
    //  request may have been modified by +link{DataSource.transformRequest()}
    // @param dsResponse (DSResponse) Response returned by the server
    // @param useDataFromRequest (boolean) If +link{DSResponse.data} is empty, should data be
    //  derived from the submitted request.
    // @return data (DataSourceRecord || Array of DataSourceRecords} Updated data.
    // @visibility external
    //<
    getUpdatedData : function (dsRequest, dsResponse, useDataFromRequest) {
        var updateData = dsResponse.data;
        // If the server failed to return the updated records, and updateCacheFromRequest is true,
        // integrate the submitted values into the cache if the operation was succesful.
        if (useDataFromRequest &&
            dsResponse.status == 0 &&
            (updateData == null ||
             (isc.isA.Array(updateData) && updateData.length == 0) ||
             isc.isAn.emptyString(updateData))
           )
        {
            this.logInfo("dsResponse for successful operation of type " + dsRequest.operationType +
                         " did not return updated record[s]. Using submitted request data to update"
                         + " ResultSet cache.", "ResultSet");
            var requestData = dsRequest.data;
            if (dsRequest.data && isc.isAn.Object(dsRequest.data)) {

                // if operationType is an update, request data will be sparse so need to combine
                // with oldValues
                if (dsRequest.operationType == "update") {
                    updateData = isc.addProperties({}, dsRequest.oldValues);

                    // Assertion - we only update one record at a time, so if submitted data is an array
                    // it will contain one entry that matches the 'oldValues'
                    if (isc.isAn.Array(requestData)) {
                        updateData = isc.addProperties(updateData, requestData[0]);
                    } else {
                        updateData = isc.addProperties(updateData, requestData);
                    }
                    updateData = [updateData];
                // for add or delete old values are irrelevant
                } else {
                    if (!isc.isAn.Array(requestData)) requestData = [requestData];
                    updateData = [];
                    for (var i = 0; i < requestData.length; i++) {
                        updateData[i] = isc.addProperties({}, requestData[i]);
                    }
                }
                //>DEBUG
                if (this.logIsDebugEnabled("ResultSet")) {
                    this.logDebug("Submitted data to be integrated into the cache:"
                                  + this.echoAll(updateData), "ResultSet");
                }
                //<DEBUG
            }
        }
        return updateData;
    },


    serializeFields : function (data, dsRequest) {


        if (!data && dsRequest != null) data = dsRequest.data;
        if (!data) return data;

        // don't try to serialize simpleType values
        if (isc.DS.isSimpleTypeValue(data)) return data;

        if (isc.isAn.Array(data)) {
            var newData = [];
            for (var i = 0; i < data.length; i++) {
                newData[i] = this.serializeFields(data[i], dsRequest);
            }
            return newData;
        } else if (this.isAdvancedCriteria(data)) {
            return this.serializeAdvancedCriteria(data);
        }
        data = isc.addProperties({}, data);


        if (data.__ref) {
            delete data.__ref;
            delete data.__module;
        }

        var fields = this.getFields();
        for (var fieldName in fields) {

            // Skip non-object values - we only care about dates
            // and nested objects which could contain dates.
            if (!isc.isAn.Object(data[fieldName])) continue;

            var field = fields[fieldName],
                type = field.type,
                typeDS = type != null ? isc.DataSource.get(type) : null;

            if (typeDS && typeDS.serializeFields) {
                data[fieldName] = typeDS.serializeFields(data[fieldName]);

            } else if (isc.isA.Date(data[fieldName])) {
                if (isc.SimpleType.getBaseType(field.type) == "date" &&
                    !isc.SimpleType.inheritsFrom(field.type, "datetime"))
                {
                    // NOTE: we choose to flag the date instead of serializing it right here
                    // because we want isc.Comm.xmlSerialize() to use xsi:type="date" for clarity
                    data[fieldName].logicalDate = true;
                } else if (isc.SimpleType.getBaseType(field.type) == "time") {
                    data[fieldName].logicalTime = true;
                }
            }
        }
        return data;
    },


    serializeAdvancedCriteria : function (data) {
        data = isc.clone(data);
        if (data.criteria) {
            for (var i = 0; i < data.criteria.length; i++) {
                data.criteria[i] = this.serializeAdvancedCriteria(data.criteria[i]);
            }
        } else {
            if (isc.isA.Date(data.value) || isc.isA.Date(data.start) || isc.isA.Date(data.end)) {
                var field = this.getField(data.fieldName);
                if (field != null) {
                    if (isc.SimpleType.getBaseType(field.type) == "date" &&
                        !isc.SimpleType.inheritsFrom(field.type, "datetime"))
                    {
                        if (data.value) data.value.logicalDate = true;
                        if (data.start) data.start.logicalDate = true;
                        if (data.end) data.end.logicalDate = true;
                    } else if (isc.SimpleType.getBaseType(field.type) == "time") {
                        if (data.value) data.value.logicalTime = true;
                        if (data.start) data.start.logicalTime = true;
                        if (data.end) data.end.logicalTime = true;
                    }
                }
            }
        }
        return data;
    },


    //> @method DataSource.getDataProtocol() [A]
    // Returns the appropriate +link{OperationBinding.dataProtocol} for a +link{DSRequest}
    // @param dsRequest (DSRequest) DataSource Request object
    // @return (DSProtocol) DataProtocol to be used for this request operation.
    // @visibility external
    //<
    getDataProtocol : function (dsRequest) {
        var operationBinding = this.getOperationBinding(dsRequest),
            service = this.getWebService(dsRequest);
        // WSDL-described web services use SOAP unless a dataProtocol has been explicitly set
        // for the operationBinding.
        // NOTE: protocol per operationBinding allows eg GET fetch, POST update
        // To avoid serializing the records when the request is clientOnly, we make this
        // method to return “clientOnly” when the dsRequest is clientOnly.
        // As this is not a valid value for DSProtocol, just an internal type, we do not
        // document "clientOnly" as a valid value for DSProtocol.
        return (operationBinding.dataProtocol != null ? operationBinding.dataProtocol :
                 isc.isA.WebService(service) ? "soap" : this.clientOnly ? "clientOnly" :
                 this.dataProtocol || "getParams");
    },

    _storeCustomRequest : function (dsRequest) {
        // clientCustom dataProtocol
        // - transformRequest has been implemented to call non SmartClient code to generate the
        //   appropriate response, and call 'processResponse()' with it.
        // - store out the request so we can retrieve it by ID when processResponse fires.
        //   Doing this before calling transformRequest should handle synchronous as well as asynch
        //   response generation code.
        // - note we do this unconditionally since it is allowed to switch a request
        //   dynamically to clientCustom in transformRequest.  If it turns out it's not a
        //   clientCustom request, we just remove the entry later
        if (!this._clientCustomRequests) this._clientCustomRequests = {};
        this._clientCustomRequests[dsRequest.requestId] = dsRequest;
    },

    // get rpcRequest properties that should be set on this dsRequest based on dataProtocol and
    // other DataSource settings.
    // NOTE:
    // - these settings will be applied to an rpcRequest which is a *copy* of the dsRequest,
    //   which is why we don't simply change the settings directly here.  See
    //   "RPCRequest vs DSRequest" comment elsewhere in this file
    // - we need to be careful to respect overrides set on the dsRequest
    getServiceInputs : function (dsRequest) {

        var operationBinding = this.getOperationBinding(dsRequest),
            service = this.getWebService(dsRequest),
            operation = this.getWSOperation(dsRequest);

        this.addDefaultCriteria(dsRequest, operationBinding);

        // call transformRequest to allow the data to be changed before it is serialized to the
        // wire.  Hang onto the data in it's original format too
        dsRequest.originalData = dsRequest.data;

        this._storeCustomRequest(dsRequest);

        this.applySendExtraFields(dsRequest);

        // offline handling may mean we never send this request
        if (!this.clientOnly) {
            if (this.fulfilledFromOffline(dsRequest)) return { dataProtocol:"clientCustom" };

            // Save a copy of the unconverted DSRequest onto itself, for later use as a key in the
            // the isc.Offline cache.  See the comments near the call to convertRelativeDates() in
            // DataSource.sendDSRequest() for details of why we do this.
            dsRequest.unconvertedDSRequest = isc.shallowClone(dsRequest);
        }

        if (this.autoConvertRelativeDates == true) {
            // convert any relative dates in criteria into absolute dates so the server
            // doesn't need to know how to handle relative dates
            if (this.logIsInfoEnabled("relativeDates")) {
                this.logInfo("Calling convertRelativeDates from getServiceInputs "+
                    "- data is\n\n"+isc.echoFull(transformedData));
            }

            var data = this.convertRelativeDates(dsRequest.data);

            if (this.logIsInfoEnabled("relativeDates")) {
                this.logInfo("Called convertRelativeDates from getServiceInputs "+
                    "- data is\n\n"+isc.echoFull(transformedData));
            }
            dsRequest.data = data;
        }

        var transformedData = this.transformRequest(dsRequest);
        // correct the common error of returning the dsRequest itself incorrectly, which is
        // never right since the dsRequest contains various widgets and other data
        // inappropriate to send to the server.
        if (transformedData !== dsRequest) {
            dsRequest.data = transformedData;
        }

        // If it was a clientCustom request, just return and wait for a call to
        // processResponse.  Note dynamic checks allows switching dataProtocol in
        // transformRequest
        var protocol = this.getDataProtocol(dsRequest),
            clientCustom = protocol == "clientCustom";
        if (clientCustom) {
            return {dataProtocol:"clientCustom"};
        } else {
            delete this._clientCustomRequests[dsRequest.requestId];
        }

        if (isc.isA.WebService(service)) {
            if (dsRequest.wsOperation == null && isc.isAn.Object(operation)) {
                dsRequest.wsOperation = operation.name;
            }
            this.logInfo("web service: " + service +
                          ", wsOperation: " + this.echoLeaf(operation), "xmlBinding");
        }

        dsRequest._sentTime = isc.timestamp();

        var dataURL = this.getDataURL(dsRequest);

        // allow dataURL to be specified as a DSRequest property.  Needed because the same
        // structure of data may come from many different URLs (such as RSS feeds or WSDL
        // files).  Also allow actionURL because we advertise that all RPCRequest props are
        // legal on DSRequests.
        dataURL = dsRequest.actionURL || dsRequest.dataURL || dataURL;

        // allow useHttpProxy to be set on operationBinding or DataSource
        if (dsRequest.useHttpProxy == null) {
            dsRequest.useHttpProxy = this._firstNonNull(operationBinding.useHttpProxy,
                                                        this.useHttpProxy);
        }

        // getParams / postParams send the request data to the server as params.
        // In addition to this, regardless of data protocol we support explicit
        // per request params, and defaultParams from the DS / binding.
        var params,
            defaultParams = operationBinding.defaultParams || this.defaultParams,
            explicitParams = dsRequest.params;

        if (defaultParams || explicitParams) {
            params = isc.addProperties({}, defaultParams, explicitParams);
        }

        var dataAsParams = protocol == "getParams" || protocol == "postParams";
        if (dataAsParams) {
            params = isc.addProperties(params || {}, dsRequest.data);
        }

        if (dataAsParams) {

            if (params) params = this.serializeFields(params, dsRequest);

            // GET or POST with form-style params
            return {
                actionURL: dataURL,
                httpMethod: dsRequest.httpMethod || (protocol == "getParams" ? "GET" : "POST"),
                params: params
            };
        }

        // remaining possibilities ("postMessage", "postXML", "soap") all POST a request body

        var serviceInputs = {
            actionURL: dataURL,
            // allow httpMethods like "PUT"
            httpMethod: dsRequest.httpMethod || "POST"
        };
        // if necessary pass params to the server as well as data in the body
        if (params) serviceInputs.params = params;

        if (protocol == "postMessage") {
            serviceInputs.data = (dsRequest.data || "").toString();
        }

        // for XML posts, serialize the data as XML
        if (protocol == "postXML" || protocol == "soap") {
            var flags = this.getSerializeFlags(dsRequest);
            var requestBody = serviceInputs.data = this.getXMLRequestBody(dsRequest);
            serviceInputs.contentType = dsRequest.contentType || "text/xml";
            this.logDebug("XML post requestBody is: " + requestBody, "xmlBinding");
        }

        // add SOAPAction HTTP header for SOAP
        if (protocol == "soap") {
            var soapAction = this._firstNonNull(operationBinding.soapAction,
                                                operation.soapAction);
            // NOTE: some engines, like Axis, require a soapAction to be present, so people who
            // don't really need a soapAction put soapAction="" into their WSDL, hoping for a
            // meaningless 'SOAPAction: ""' header to get by Axis.
            if (soapAction == null) soapAction = '""';
            serviceInputs.httpHeaders = isc.addProperties({ SOAPAction : soapAction },
                                                            dsRequest.httpHeaders);

            var spoofResponses = isc.isA.WebService(service) &&
                                 this._firstNonNull(operationBinding.spoofResponses,
                                                    this.spoofResponses);

            if (spoofResponses) {
                serviceInputs.spoofedResponse = service.getSampleResponse(operation.name);
                this.logInfo("Using spoofed response:\n" + serviceInputs.spoofedResponse, "xmlBinding");
            }

        }

        if (this.logIsDebugEnabled("xmlBinding")) {
            this.logDebug("serviceInputs are: " + this.echo(serviceInputs), "xmlBinding");
        }
        return serviceInputs;
    },

    addDefaultCriteria : function (dsRequest, operationBinding) {
        var defaultCriteria = operationBinding.defaultCriteria || this.defaultCriteria;

        // nothing to do if there's no defaultCriteria
        if (!defaultCriteria) return;

        defaultCriteria = isc.addProperties({}, defaultCriteria);
        if (defaultCriteria && dsRequest.operationType == "fetch") {
            if (this.isAdvancedCriteria(dsRequest.data)) {
                // combineCriteria will retain references to like fields from both criteria sets,
                // which is inappropriate for this use case - we want the real values to hide the
                // default values, otherwise we'll end up with something like
                // "myField = 'defaultValue' AND myField = 'realValue'".  This isn't straightforward
                // because AdvancedCriteria can be arbitrarily complex.  So, we'll assume that the
                // defaultCriteria is always simple (and document it that way) and remove from it
                // any property that is named anywhere in the existing criteria
                var fieldNames = this.mineCriteriaFieldNames(dsRequest.data);
                for (var key in defaultCriteria) {
                    if (fieldNames.contains(key)) delete defaultCriteria[key];
                }
            }
            // nothing to do if all defaultCriteria were removed (because they're already used!)
            if (isc.getValues(defaultCriteria).length > 0) {
                dsRequest.data = isc.DataSource.combineCriteria(dsRequest.data,
                                    defaultCriteria, "and", null);
            }
        }
    },

    mineCriteriaFieldNames : function (criteria) {
        var fieldNames = [];
        if (!criteria.criteria) return fieldNames;

        for (var i = 0; i < criteria.criteria.length; i++) {
            if (criteria.criteria[i].criteria) {
                fieldNames.addList(this.mineCriteriaFieldNames(criteria.criteria[i]));
            } else {
                fieldNames.add(criteria.criteria[i].fieldName);
            }
        }
        return fieldNames;
    },

    applySendExtraFields : function (dsRequest) {
        // If sendExtraFields is false, remove any non-ds fields from the record(s) in request.data
        // before calling transformRequest

        if (!this.sendExtraFields) {
            var data = dsRequest.data;
            if (!isc.isAn.Array(data)) data = [data];
            for (var i = 0; i < data.length; i++) {
                var record = data[i];
                if (!isc.isAn.Object(record)) continue;

                for (var field in record) {
                    if (!this.getField(field)) delete record[field];
                }
            }
        }
    },

    //> @method DataSource.processResponse() (A)
    // Process a dsResponse for a request initiated by a DataSource with
    // +link{OperationBinding.dataProtocol,dataProtocol:"clientCustom"}.
    // <code>requestId</code> parameter should be dsRequest.requestId as found on the dsRequest
    // passed to +link{dataSource.transformRequest()}.
    // <P>
    // You must provide a response for both error and non-error cases.  For an error case, a
    // sufficient response is:
    // <pre>
    // { status : -1 }
    // </pre>
    // @param requestId (string) requestId attribute from the associated dataSource request object
    // @param dsResponse (DSResponse properties) Configuration for the dsResponse
    // @visibility external
    //<
    processResponse : function (requestId, dsResponse) {
        var dsRequest = this._clientCustomRequests[requestId];
        if (dsRequest == null) {
            this.logWarn("DataSource.processResponse(): Unable to find request corresponding to ID "
                        + requestId + ", taking no action.");
            return;
        }
        delete this._clientCustomRequests[requestId];

        // assume success
        if (dsResponse.status == null) dsResponse.status = 0;
        if (dsResponse.status == 0) {
            // If start/endRow are unset check for data length
            var data = dsResponse.data;
            if (data == null) dsResponse.data = data = [];
            if (dsResponse.startRow == null) dsResponse.startRow = dsRequest.startRow || 0;
            if (dsResponse.endRow == null) dsResponse.endRow = dsResponse.startRow + data.length;
            if (dsResponse.totalRows == null) {
                dsResponse.totalRows = Math.max(dsResponse.endRow, data.length);
            }
        }

        this._completeResponseProcessing(data, dsResponse, dsRequest);
    },

    // An undocumented threshold over which the call to getClientOnlyResponse(), when
    // handling the data source reply of a client only data source, is to be executed
    // asynchronously.
    _asyncClientOnlyResponseThreshold: 200,

    _handleClientOnlyReply : function (rpcResponse, data, rpcRequest) {
        var serverData = this.cacheAllData && !this.clientOnly && this.cacheResultSet ?
                this.cacheResultSet.getAllRows() : null,
            dsRequest = rpcRequest._dsRequest;

        // getClientOnlyResponse() is called asynchronously when the number of records is over
        // the configurable threshold _asyncClientOnlyResponseThreshold and copyLocalResults is
        // true.
        var callback = function (dsResponse) {
            // use by the Developer Console RPC tab to show clientOnly responses
            rpcResponse.clientOnlyData = dsResponse ? dsResponse.data : null;

            this._completeResponseProcessing(data, dsResponse, dsRequest, rpcResponse, rpcRequest);
        };
        if (this.copyLocalResults &&
            this.getClientOnlyResponse === isc.DataSource.getInstanceProperty("getClientOnlyResponse"))
        {
            this._asyncGetClientOnlyResponse(dsRequest, serverData, callback);
        } else {
            var dsResponse = this.getClientOnlyResponse(dsRequest, serverData);
            callback.call(this, dsResponse);
        }
    },


    _asyncGetClientOnlyResponse : function (request, serverData, callback) {
        //!OBFUSCATEOK

        var operationType = request.operationType,
            response = {
                status:0
            };

        if (!(operationType == "fetch" || operationType == "select" || operationType == "filter")) {
            // The synchronous version of getClientOnlyResponse() should be sufficient for handling
            // the other operation types (remove, delete, add, insert, replace, update, and validate).
            callback.apply(this, [this.getClientOnlyResponse(request, serverData)]);
            return;
        }

        // initialize the spoofed dataset

        serverData = serverData || this.testData;

        var cacheData = this.cacheData,
            testData = this.testData,
            setCacheDataAndTestDataToServerData = false;

        // if we have serverData but no testData, it's a cacheAllData result - if we're also in
        // clientOnly mode, we need to set up the clientOnly cache - set both cacheData and the
        // now deprecated testData to the same array
        if (serverData && !this.testData && this.clientOnly) {
            cacheData = testData = serverData;
            setCacheDataAndTestDataToServerData = true;
        }

        if (!serverData || isc.isA.String(serverData)) {
            if (isc.isA.String(serverData)) {
                // ID or expression
                //>DEBUG
                this.logInfo(this.ID + " datasource: using testData property as data");
                //<DEBUG
                cacheData = testData = isc.eval(serverData);
                setCacheDataAndTestDataToServerData = true;
            } else if (window[this.ID + "TestData"]) {
                // dataset loaded in page under canonical name
                //>DEBUG
                this.logInfo(this.ID + " datasource: using " + this.ID + "TestData object as data");
                //<DEBUG
                cacheData = testData = window[this.ID + "TestData"];
                setCacheDataAndTestDataToServerData = true;
            } else {
                // initialize to empty list
                //>DEBUG
                this.logInfo(this.ID + " datasource: testData property and " + this.ID +
                             "TestData object not found, using empty list as data");
                //<DEBUG
                cacheData = testData = [];
                setCacheDataAndTestDataToServerData = true;
            }
            serverData = testData;
        }

        var criteria = request.data;
        // support multiple where clause format (don't think this is actually valid any
        // more)
        if (isc.isAn.Array(criteria)) criteria = criteria[0];
        var filteredData = this.applyFilter(serverData, criteria, request),
            resultData = filteredData;

        if (request.startRow != null) {
            // simulate paging
            var startRow = request.startRow,
                endRow = request.endRow,
                totalRows = filteredData.length;

            // server handles sort, so sort the data by the requested direction

            var sortProperty = isc.shallowClone(request.sortBy);
            if (sortProperty) {
                if (!isc.isAn.Array(sortProperty)) sortProperty = [sortProperty];

                if (isc.isAn.Object(sortProperty[0])) {
                    // if this is an array or SortSpecifiers, map them to sortBy strings
                    sortProperty = isc.DS.getSortBy(sortProperty)
                }

                var numSortProperties = sortProperty.length,
                    sortDirs = new Array(numSortProperties),
                    contexts = new Array(numSortProperties);

                for (var i = 0; i < numSortProperties; ++i) {
                    var sortDirection = true;
                    if (sortProperty[i].startsWith("-")) {
                        sortProperty[i] = sortProperty[i].substring(1);
                        sortDirection = false;
                    }
                    sortDirs[i] = sortDirection;
                    contexts[i] = this;
                }
                filteredData.sortByProperties(sortProperty, sortDirs, null, contexts);
            }

            // cap the endRow at one after last valid index (it's exclusive)
            endRow = Math.min(endRow, totalRows);
            // slice out from startRow to endRow (note: slice is non-inclusive at the end)
            resultData = filteredData.slice(startRow, endRow);
            response.startRow = startRow;
            response.endRow = endRow;
            response.totalRows = totalRows;
        }

        // shallow copy the results.  This allows test or example cases where the
        // "server data set" is changing independently of the client results
        var finish = function () {
            response.data = resultData;

            this._trackClientOnlyChanges(response, operationType);

            if (setCacheDataAndTestDataToServerData) {
                this.cacheData = this.testData = serverData;
            }

            callback.call(this, response);
        };
        if (this.copyLocalResults) {
            this._asyncCopyLocalResults(0, resultData.length, resultData, finish);
        } else {
            finish.call(this);
        }
    },

    _asyncCopyLocalResults : function (startIndex, endIndex, resultData, callback) {
        var finalIndex = endIndex,
            isSGWT = isc.Browser.isSGWT;
        endIndex = Math.min(endIndex, startIndex + this._asyncClientOnlyResponseThreshold);
        for (var i = startIndex; i < endIndex; ++i) {
            if (this.deepCopyLocalResults) {
                resultData[i] = isc.clone(resultData[i]);
            } else {
                resultData[i] = isc.addProperties({}, resultData[i]);
            }
            // Clear out SGWT's __ref/__module pointers so that if the cacheData is created in
            // SGWT, Record.getOrCreateRef() won't return the same Java Record object for the
            // copied result record.
            if (isSGWT) {
                resultData[i][isc.gwtRef] = null;
                resultData[i][isc.gwtModule] = null;
            }
        }
        if (endIndex == finalIndex) {
            callback.call(this);
        } else {
            this.delayCall("_asyncCopyLocalResults", [endIndex, finalIndex, resultData, callback], 0);
        }
    },

    _handleCustomReply : function (rpcResponse, data, rpcRequest) {
        var dsResponse = {
            data : data,
            startRow : 0,
            endRow : 0,
            totalRows : 0,
            status : 0
        };
        var dsRequest = rpcRequest._dsRequest;
        this._completeResponseProcessing(data, dsResponse, dsRequest, rpcResponse, rpcRequest);
    },

    _handleJSONReply : function (rpcResponse, data, rpcRequest) {
        var dsRequest = rpcRequest._dsRequest,
            recordXPath = this.getOperationBinding(dsRequest).recordXPath || this.recordXPath;

        // log the raw object reply if we didn't just eval it from text (if we eval it from
        // text we've already logged the raw response)
        if ((rpcResponse._wasJSONTextReply || rpcResponse._wasCSVTextReply) && this.logIsDebugEnabled("xmlBinding")) {
            this.logDebug("Raw response data: " + this.echoFull(data), "xmlBinding");
        }

        // Hang onto the raw JSON object returned by the server. This allows transformResponse
        // to actually do something useful with the data returned (EG, pick up meta-data)
        var rawData = data;

        // Don't try to process null data - this can now happen if we get an invalid
        // HTTP response (404, 500, etc) and willHandleError == true
        if ( data) {
            if (recordXPath) {
                // extract relevant records via XPath
                data = isc.xml.selectObjects(data, recordXPath);
                this.logInfo("JSON recordXPath: '" + recordXPath +
                             "', selected: " + this.echoLeaf(data), "xmlBinding");
            }

            var responseDataSchema = this.getOperationBinding(dsRequest).responseDataSchema;
            data = this.recordsFromObjects(data, responseDataSchema);

            if (this.logIsDebugEnabled("xmlBinding")) {
                this.logDebug("Validated dsResponse.data: " + isc.echoFull(data), "xmlBinding");
            }

            // derive the default response
            var dsResponse = {
                data : data,
                startRow : dsRequest.startRow || 0,
                status : 0 // assume success
            };

            dsResponse.endRow = dsResponse.startRow + Math.max(0, data.length);
            dsResponse.totalRows = Math.max(dsResponse.endRow, data.length);

        } else {
            // derive the default error response
            var status = rpcResponse.status;
            if (status == 0 || status == null) status = -1;
            var dsResponse = {
                status : status,
                data:rpcResponse.data
            };
rawData=rpcResponse.results;
        }

        this._completeResponseProcessing(rawData, dsResponse, dsRequest, rpcResponse, rpcRequest);
    },

    _handleCSVTextReply : function (rpcResponse, csvText, rpcRequest) {
        if (rpcResponse.status != 0) return;

        var settings = isc.TextImportSettings.create({
            hasHeaderLine: true,
            trim: true
        });
        isc.TextImportSettings._importAsJsonObjects = true;
        var jsonObjects = this.recordsFromText(rpcResponse.data, settings);
        isc.TextImportSettings._importAsJsonObjects = false;

        rpcResponse._wasCSVTextReply = true;
        this._handleJSONReply(rpcResponse, jsonObjects, rpcRequest);
    },

    _handleJSONTextReply : function (rpcResponse, jsonText, rpcRequest) {
        //!DONTOBFUSCATE

        // If the original request came from RestDataSource and formed part of a multi-op
        // queue, queue-handling code in RPCManager will have eval'd the response already,
        // so what is passed in here as "jsonText" is actually a real JS object
        if (rpcRequest.isRestRequest && !isc.isA.String(jsonText)) {
            var jsonObjects = jsonText;
        } else {
            // Don't try to process jsonText if we didn't get a valid response
            if (rpcResponse.status >= 0) {
                var evalText = jsonText;
                // Strip the json prefix / suffix if supplied, and transport was not scriptInclude
                if (rpcRequest.transport != "scriptInclude" && evalText != null) {
                    // Strip whitespace before checking for specified prefix / suffix
                    var re;
                    if (this.jsonPrefix) {
                        re = new RegExp(/^\s*/);
                        evalText = evalText.replace(re, "");
                        if (evalText.startsWith(this.jsonPrefix)) {
                            evalText = evalText.substring(this.jsonPrefix.length);
                        } else {
                            this.logInfo("DataSource specifies jsonPrefix, but not present in " +
                                         "response returned from server. Processing response anyway.");
                        }
                    }
                    if (this.jsonSuffix) {
                        re = new RegExp(/\s*$/)
                        evalText = evalText.replace(re, "");
                        if (evalText.endsWith(this.jsonSuffix)) {
                            evalText = evalText.substring(0, (evalText.length - this.jsonSuffix.length));
                        } else {
                            this.logInfo("DataSource specifies jsonSuffix, but not present in " +
                                         "response returned from server. Processing response anyway.");
                        }
                    }
                }

                var wasInvalid = false;
                try {
                    var jsonObjects;
                    if (rpcRequest.useStrictJSON) {
                        jsonObjects = isc.Class.parseStrictJSON(evalText, rpcRequest.jsonReviver);
                    } else {
                        // Note: FF1.0.7 bug: curly must be escaped in the following regex.  Not an issue in IE
                        // or FF1.5
                        if (evalText && evalText.match(/^\s*\{/)) {
                            // eval barfs on "{ ...  }", thinking it's a closure
                            evalText = "var evalText = " + evalText + ";evalText;"
                        }
                        jsonObjects = isc.eval(evalText);
                    }
                } catch (e) {

                    rpcResponse.status = -1;
                    rpcResponse.data = "Error: server returned invalid JSON response";
                    this.logWarn("Error evaluating JSON: " + e.toString() + ", JSON text:\r" + jsonText);
                    wasInvalid = true;
                }

                // warn and bail if server response object evals to null.
                // This can happen if the response is "", or if it's something like "var foo = {};"
                // where the result of the eval is undef.
                // Avoids a JS error in handleJSONReply
                // In case of invalid JSON received, rpcResponse has been setup previously
                if (jsonObjects == null && !wasInvalid) {
                    rpcResponse.status = -1;
                    rpcResponse.data = "Error: server returned invalid JSON response";
                    this.logWarn("Evaluating JSON reply resulted in empty value. JSON text:\r" + this.echo(jsonText));
                    jsonObjects = jsonText;
                }

                // Hack to let _handleJSONReply know this JSON response came from text, for logging
                // purposes
                rpcResponse._wasJSONTextReply = true;
            }
        }

        this._handleJSONReply(rpcResponse, jsonObjects, rpcRequest);
    },

    //> @method dataSource.recordsFromObjects() [A]
    // Transform a list of Objects to DataSource records.
    // <P>
    // The value for each field is extracted from the XML according to the
    // rules described under +link{attr:DataSourceField.valueXPath}.
    //<
    recordsFromObjects : function (data, schema) {

        // normalize to Array and apply schema
        if (!isc.isAn.Array(data)) data = [data];

        // skipping validation means eg date values specified as Strings won't become Dates,
        // valueXPath and dropExtraFields won't apply, etc.  But for large data volumes where
        // these features aren't required, this is faster.  Undocumented for now.
        if (this.skipJSONValidation) return data;

        for (var i = 0; i < data.length; i++) {
            data[i] = this.validateJSONRecord(data[i], null, schema);
        }
        return data;
    },

    validateJSONRecord : function (record, disableXPath, schema) {
        // If no record was given, return null.
        if (!record) {
            return null;
        }
        if (schema == null) {
            schema = this;
        }
        var fieldNames = this.getFieldNames(),
            result = {};
        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = schema.getField(fieldName),
                fieldValue;

            if (field.valueXPath && !disableXPath) {
                fieldValue = isc.xml.selectObjects(record, field.valueXPath, true);
            } else {
                fieldValue = record[fieldName];
            }

            if (field.getFieldValue) {
                if (!isc.isA.Function(field.getFieldValue)) {
                    isc.Func.replaceWithMethod(field, "getFieldValue",
                                                     "record,value,field,fieldName");
                }
                fieldValue = field.getFieldValue(record, fieldValue, field, fieldName);
            }

            var undef;
            if (fieldValue !== undef) {
                // validation fieldValue, if it is complex type
                var fieldDS = isc.DS.get(field.type);
                if (fieldDS && !(fieldDS.skipJSONValidation)) {
                    if (!(isc.isAn.Array(fieldValue))) {
                        fieldValue = fieldDS.validateJSONRecord(fieldValue);
                    } else {
                        for (var j = 0; j < fieldValue.length; j++) {
                            fieldValue[j] = fieldDS.validateJSONRecord(fieldValue[j]);
                        }
                    }
                }

                //this.logWarn("validating value: " + fieldValue +
                //             " of field: " + this.echo(field));
                // field is multiple:true
                if (isc.isAn.Array(fieldValue)) {
                    var childValue = [];
                    for (var j = 0; j < fieldValue.length; j++) {
                        childValue.push(this.validateFieldValue(field, fieldValue[j]));
                    }
                    result[fieldName] = childValue;
                } else {
                    result[fieldName] = this.validateFieldValue(field, fieldValue);
                }
            }
        }

        if (schema.dropExtraFields) return result;


        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i];
            record[fieldName] = result[fieldName]
        }
        return record;
    },

    // XML Request Formation
    // ---------------------------------------------------------------------------------------

    // get the schema to be used to serialize JS data to XML.
    // Also takes a dsRequest.
    getMessageSerializer : function (operationType, operationId) {
        // either:
        // - an operationBinding has been specified with a wsOperation, so we use that, or
        // - no operationBinding was specified, so getOperationBinding() returns this, and then:
        //   - if this.wsOperation has been set, we use it
        //   - otherwise, we don't seem to be connected to a web service, so we use this
        //     DataSource as the message schema
        var operationBinding = this.getOperationBinding(operationType, operationId);
        if (operationBinding.wsOperation) {
            var service = this.getWebService(operationType, operationId);
            return service.getMessageSerializer(operationBinding.wsOperation);
        }
        return this;
    },

    //> @method dataSource.getXMLRequestBody() [A]
    // Get the XML to be posted to the dataURL based on the passed DSRequest.
    // <P>
    // This API is intended to be overridden in order to integrate with web services that
    // expect XML messages rather than simple HTTP parameters, but lack a
    // WSDL description.  For WSDL-described web services, having loaded the service
    // description, SmartClient knows the correct XML message structure, so customization is
    // best done by modifying the JavaScript data that is used to form the message.
    //
    // @see XMLTools.loadWSDL() for integration with WSDL-described web services
    //
    // @param dsRequest (DSRequest) the request to encode as an XML message.
    // @return (String) the entire XML request as a String, including SOAP envelope if SOAP is used
    // @visibility xmlBinding
    //<
    getXMLRequestBody : function (dsRequest, flags) {
        if (isc._traceMarkers) arguments.__this = this;

        var requestBody = isc.SB.create(),
            protocol = this.getDataProtocol(dsRequest);

        // start envelope and headers
        if (protocol == "soap") {
            requestBody.append(this.getSoapStart(dsRequest), "\r");
            requestBody.append(this.getSoapBody(dsRequest, flags));
            requestBody.append("\r", this.getSoapEnd(dsRequest));
        } else {
            if (this.messageStyle == "template") {
                requestBody.append(this._createTemplatedRequestBody(dsRequest));
            } else {
                var schema = this.getMessageSerializer(dsRequest);
                var flags = this.getSerializeFlags(dsRequest, flags);
                requestBody.append(schema.xmlSerialize(dsRequest.data, flags));
            }
        }

        if (this.logIsDebugEnabled("xmlComm")) {
            this.logDebug("outbound XML message: " + requestBody, "xmlComm");
        }

        return requestBody.toString();
    },


    _createTemplatedRequestBody : function (dsRequest) {
        var requestBody = isc.SB.create(),
            template = this.soapBodyTemplate,
            message;


            message = template.evalDynamicString(this, dsRequest);


        return message;
    },

    // WebService-related helper methods
    getSchemaSet : function () {
        return isc.SchemaSet.get(this.schemaNamespace);
    },

    hasWSDLService : function (dsRequest) {
        return isc.isA.WebService(this.getWebService(dsRequest));
    },

    getWebService : function (dsRequest) {
        var operationBinding = this.getOperationBinding(dsRequest),
            serviceNamespace = (dsRequest ? dsRequest.serviceNamespace : null)
                                    || operationBinding.serviceNamespace || this.serviceNamespace,
            serviceName = (dsRequest ? dsRequest.serviceName : null)
                                    || operationBinding.serviceName || this.serviceName;

        // see if we have a WebService instance with this serviceName / serviceNamespace
        var service;
        if (serviceName) service = isc.WebService.getByName(serviceName, serviceNamespace);
        else service = isc.WebService.get(serviceNamespace);

        if ((serviceNamespace != null || serviceName != null) && service == null) {
            this.logWarn("Could not find WebService definition: " +
                         (serviceName ? "serviceName: " + serviceName : "") +
                         (serviceNamespace ? "   serviceNamespace: " + serviceNamespace : "")
                         + this.getStackTrace());
        }

        // if we don't, then the DataSource itself is expected to have all the relevant
        // properties
        return service || this;
    },

    getWSOperation : function (dsRequest) {

        var operationBinding = this.getOperationBinding(dsRequest),
            wsOperation = (isc.isAn.Object(dsRequest) ? dsRequest.wsOperation : null) ||
                            operationBinding.wsOperation || this.wsOperation,
            service = this.getWebService(dsRequest);

        if (wsOperation != null && isc.isA.WebService(service)) {
            var returnOp = service.getOperation(wsOperation);
            if (!returnOp) {
                isc.logWarn("DataSource.getWSOperation() : could not retrieve the operation "
                    + wsOperation);
            }
            return returnOp;
        }
        return this;
    },

    getDataURL : function (dsRequest) {
        var operationBinding = this.getOperationBinding(dsRequest);

        // if we have a real operationBinding definition (we're not using "this" as the
        // operationBinding definition) and it has a dataURL set, use that.  This allows eg
        // fetch via GET from one URL, save via POST to another
        if (operationBinding != this && operationBinding.dataURL) return operationBinding.dataURL;

        // NOTE: allow dataURL to be specified even on a DataSource that is using a web
        // service.  This is a simple way to spoof web service responses for a single
        // DataSource; using webService.setLocation() allows you to put the spoofing at the
        // WebService level
        if (this.dataURL != null) return this.dataURL;

        // otherwise if we have a WSDL-derived WebService definition, ask it for a dataURL
        // (WSDL services can't really have multiple URLs, but it's useful to set a different
        // URL per operation for spoofing during testing)
        if (this.hasWSDLService(dsRequest)) {
            var service = this.getWebService(dsRequest);
            return service.getDataURL(this.getWSOperation(dsRequest).name);
        }

        return this.dataURL;
    },

    //> @attr dataSource.globalNamespaces (Object : ... : IRW)
    // Namespaces definitions to add to the root element of outbound XML messages sent to a web
    // service, as a mapping from namespace prefix to namespace URI.
    // <P>
    // The default value is:
    // <pre>
    //   globalNamespaces : {
    //      xsi: "http://www.w3.org/2001/XMLSchema-instance",
    //      xsd: "http://www.w3.org/2001/XMLSchema"
    //   },
    // </pre>
    // This default value allows the use of the xsi:type and xsi:nil attributes without further
    // declarations.
    // <P>
    // Note that some web services will only accept specific revisions of the XML Schema URI.
    // If xsi-namespaced attributes seem to be ignored by an older webservice, try the URI
    // "http://www.w3.org/1999/XMLSchema-instance" instead.
    //
    // @serverDS allowed
    // @visibility xmlBinding
    //<
    getGlobalNamespaces : function (dsRequest) {
        var service = this.getWebService(dsRequest),
            namespaces = this.globalNamespaces;
        if (service && service.globalNamespaces) {
            namespaces = isc.addProperties({}, namespaces, service.globalNamespaces);
        }
        return namespaces;
    },

    getSoapStart : function (dsRequest) {
        // allow centralized overrides of getSoapStart/End via overrides on a WebService object
        var service = this.getWebService(dsRequest);
        if (service.getSoapStart) return service.getSoapStart(dsRequest);

        return isc.SB.concat(isc.DataSource._soapEnvelopeStart,
                             isc.xml._generateNamespaces(this.getGlobalNamespaces(dsRequest),
                                                         null, "         "),
                             ">", isc.DataSource._soapHeaderStart,
                             this.getSoapHeader(dsRequest),
                             isc.DataSource._soapHeaderEnd);
    },
    getSoapHeader : function (dsRequest) {
        var service = this.getWebService(dsRequest);
        if (service.getSoapHeader) return service.getSoapHeader(dsRequest);

        // take data from either the request or from the service
        var headerData = dsRequest.headerData || service.getHeaderData(dsRequest);

        if (!headerData) return;

        this.logDebug("headerData is: " + this.echo(headerData), "xmlBinding");

        // get schema for the headers
        var allHeaderSchema = service.getInputHeaderSchema(this.getWSOperation(dsRequest)) ||
                                isc.emptyObject;

        // WSDL/SOAP blows: in SOAP the <wsdl:part>s of a <wsdl:message> do not have any name or
        // means of referring to them other than by the combination of the wsdl:message@name and
        // wsdl:part@name attributes.  When asking developers to specify data to be serialized
        // for each part, we rely on just the part name.  It would be perverse to declare a
        // soap header or body that uses two identically named parts of different messages, but
        // it appears to be legal.
        // Relevant spec: http://www.w3.org/TR/wsdl#_soap:header
        var output = "", flatHeaders = dsRequest.useFlatHeaderFields;
        for (var partName in headerData) {
            var headerSchema = allHeaderSchema[partName];
            if (headerSchema != null) {
                //this.logWarn("serialized partName: " + partName +
                //             " with schema " + headerSchema);
                if (isc.isA.DataSource(headerSchema)) {
                    output += headerSchema.xmlSerialize(headerData[partName],
                                                        { useFlatFields: flatHeaders })
                } else {
                    // special handling for simpleType headers:
                    // The wsdl:message declaration, which becomes a DataSource, does not have
                    // a namespace (it doesn't belong to an xs:schema), however the wsdl:parts
                    // refer to types declared in an xs:schema block that must be namespaced.
                    // When a wsdl:part refers to a simpleType element, there won't be a
                    // DataSource for the element, only a field, hence the field has to carry
                    // the namespace via the special partNamespace property.
                    // headerSchema, in this case, is a field definition.
                    output += "\r     " + this._serializeSimpleTypeTag(partName, headerSchema,
                                    headerData[partName], headerSchema.partNamespace);
                }
            } else {
                this.logWarn("headerData passed for SOAP header partName: " + partName +
                             ", no schema available, not outputting");
            }
        }
        return output;
    },
    getSoapBody : function (dsRequest, flags) {
        if (isc._traceMarkers) arguments.__this = this;

        var soapBody = isc.SB.create(),
            service = this.getWebService(dsRequest),
            soapStyle = this.getSoapStyle(dsRequest),
            wsOperation = this.getWSOperation(dsRequest),

            // pass in a "flags" object that already has nsPrefixes, and reuse this flags
            // object to serialize each soap:body part.  This will capture all required
            // namespaces so we can output them as part of the soap:body element.
            // For readability of the SOAP message, include the explicitly specified
            // namespace prefixes.
            userPrefixes = this.xmlNamespaces ? isc.makeReverseMap(this.xmlNamespaces) : null,
            flags = isc.addProperties({
                nsPrefixes : isc.addProperties({}, userPrefixes)
            }, flags),

            // A soap:body has exactly one wsdl:message, represented by a DataSource, where each
            // DataSource field represents a wsdl:part of the message.
            messageSchema = flags.generateResponse ? service.getResponseMessage(wsOperation.name)
                                                   : service.getRequestMessage(wsOperation.name),
            // 1. the soap:body can have multiple parts, either because:
            // - soap:body@parts is unspecified and the message has multiple parts
            // - soap:body@parts is specified and is a list
            // 2. each part can be just a simple type element or a complex element
            //
            // Relevant spec: http://www.w3.org/TR/wsdl#_soap:body
            // - NOTE: not explicitly covered in the spec above, but based on WSDL4J APIs and
            //   other samples, soap:body@parts can be a space-separated *list* of part names
            //
            // Contrast this with soap:headers:
            // 1. each soap:header can have a different message
            // 2. each soap:header must specify a part
            bodyPartNames = flags.bodyPartNames ||
                                service.getBodyPartNames(wsOperation.name, flags.generateResponse);

        flags = this.getSerializeFlags(dsRequest, flags);


        isc.Comm.omitXSI = wsOperation.inputEncoding != "encoded";
        var oldXMLSchemaMode = isc.Comm.xmlSchemaMode;
        isc.Comm.xmlSchemaMode = true;

        var indent = "        ";

        if (soapStyle == "rpc") {
            // for RPC-style SOAP, SOAP body's outermost element should be named after the
            // operation and namespaced with the operation namespace
            soapBody.append(
                "\n", indent,
                isc.Comm._xmlOpenTag(wsOperation.name, null,
                                     wsOperation.inputNamespace, "opNS", true),
                ">"
            );
            indent += "    ";
            // otherwise the message definition as such does not produce outputs, only it's
            // parts do
        }

        this.logInfo("soap:body parts in use: '" + bodyPartNames +
                     "', soapStyle: " + soapStyle, "xmlSerialize");
        if (this.logIsDebugEnabled("xmlSerialize")) {
            this.logDebug("SOAP data is: " + this.echoFull(dsRequest.data), "xmlSerialize");
        }

        for (var i = 0; i < bodyPartNames.length; i++) {
            var partName = bodyPartNames[i];

            // for document-style SOAP, if there is exactly one message part (the most common
            // style by far), use the entirety of the request data for that part.
            // This means that when a message is supposed to look like this:
            //    <login>
            //       <username>bob</username>
            //       <password>mebob</password>
            //    </login>
            // The JS data you need to pass is:
            //    { username:"bob", password:"mebob" }
            // .. instead of the surprising and less obvious:
            //    { login : { username:"bob", password:"mebob" } }
            // Note that this works in conjunction with webService.getInputDS(), which for a
            // single part message will return the schema of that single part, such that a form
            // bound to that schema will produce the appropriate values to pass as
            // dsRequest.data.
            var data = bodyPartNames.length < 2 && soapStyle == "document" ?
                            dsRequest.data : (dsRequest.data ? dsRequest.data[partName] : null);

            var partXML = messageSchema.getMessagePart(partName, data, flags, indent);

            soapBody.append("\r" + indent + partXML);
        }

        if (soapStyle == "rpc") {
            soapBody.append(
                "\n", "        ", isc.Comm._xmlCloseTag(wsOperation.name,
                                                        wsOperation.inputNamespace, "opNS")
            );
        }

        // restore global XML serialization settings
        isc.Comm.omitXSI = null;
        isc.Comm.xmlSchemaMode = oldXMLSchemaMode;

        return isc.SB.concat("    ", isc.DS._soapBodyStart,
                             this.outputNSPrefixes(flags.nsPrefixes, "        "),
                             ">",
                             soapBody.toString(),
                             "\r    ", isc.DS._soapBodyEnd);
    },

    // serialize a <wsdl:part> of a <wsdl:message>
    getMessagePart : function (partName, data, flags, indent) {
        if (isc._traceMarkers) arguments.__this = this;

        var partField = this.getPartField(partName),
            partSchema = this.getSchema(partField.type, partField.xsElementRef ? "element" : null),
            shouldLog = this.logIsInfoEnabled("xmlSerialize");

        // As with DataSource fields in general, each wsdl:part of a wsdl:message can be simple
        // or complex type
        if (isc.isA.DataSource(partSchema)) {
            if (shouldLog) {
                this.logInfo("soap:body part '" + partName +
                             "' is complex type with schema: " + partSchema +
                             " has value: " +
                             (this.logIsDebugEnabled("xmlSerialize") ?
                                    this.echo(data) : this.echoLeaf(data)), "xmlSerialize");
            }
            // a wsdl:part can specify complex type via @type or @element.  With @type, the
            // generated tag should match the part name.  With @element, it should match the
            // element name, which the partSchema will use if we just pass tagName as null.
            var tagName = partField.xsElementRef ? null : partName;
            return partSchema.xmlSerialize(data, flags, indent, tagName);
        } else {
            // if a simpleType part receives complex type data, assume they tried to specify
            // data as for example:
            //   { partName : value}
            // .. or ..
            //   { simpleTypeElementName : value }
            // .. and automatically find the simpleType value under those property names if
            // present
            if (data != null && !isc.DS.isSimpleTypeValue(data)) {
                 data = data[partField.name] || data[partName] || data;
            }

            if (shouldLog) {
                this.logInfo("soap:body part '" + partName +
                             "' is of simple type '" + partField.type + "'" +
                             " has value: '" + this.echoLeaf(data) + "'", "xmlSerialize");
            }

            // if wsdl:part referred via element= to an element of simpleType in an imported
            // schema file.  This becomes a trivial isc.SimpleType but carries the correct
            // namespace.
            var simpleType = this.getType(partField.type),
                partNamespace = partField.partNamespace;
            if (!partNamespace && simpleType && simpleType.schemaNamespace) {
                partNamespace = simpleType.schemaNamespace;
            }

            //this.logWarn("partField: " + this.echo(partField) +
            //             ", simpleType: " + this.echo(simpleType) +
            //             ", partNamespace: " + partNamespace);

            return this._serializeSimpleTypeTag(partField.name || partName, partField, data,
                                                partNamespace, flags);
        }
    },


    // for a WSDLMessage, get the field that correspondes to a message <part>.
    // the field for a wsdl:part with @element will have field.name matching the
    // @element name (correct for serialization) but will also have a partName
    // attribute matching the "part" from the operation.inputHeaders/outputHeaders.
    // See schemaTranslator.xsl.
    getPartField : function (partName) {
        var partField = isc.getValues(this.getFields()).find("partName", partName);
        if (partField != null) return partField;
        return this.getField(partName);
    },
    getSoapEnd : function (dsRequest) {
        // allow centralized overrides of getSoapStart/End via overrides on a WebService object
        var service = this.getWebService(dsRequest);
        if (service.getSoapEnd) return service.getSoapEnd(dsRequest);

        return isc.DataSource._soapEnvelopeEnd;
    },

    getSoapStyle : function (dsRequest) {
        if (!this.hasWSDLService(dsRequest)) return "document";
        return this.getWebService(dsRequest).getSoapStyle(this.getWSOperation(dsRequest).name);
    },

    // get all the flags this DataSource should use for XML serialization, based on a
    // DSRequest and settings in the relevant operationBinding and the DataSource as a whole
    getSerializeFlags : function (dsRequest, flags) {
        flags = isc.addProperties({
            soapStyle : this.getSoapStyle(dsRequest)
        }, flags);

        // a few properties can be set on the operationBinding
        var operationBinding = this.getOperationBinding(dsRequest);

        // if useFlatFields is set, assume flat inbound data to be mapped automatically to
        // hierarchical XML message
        flags.flatData = this._firstNonNull(dsRequest.useFlatFields,
                                            operationBinding.useFlatFields,
                                            this.useFlatFields);
        flags.recursiveFlatFields = this._firstNonNull(dsRequest.recursiveFlatFields,
                                                       operationBinding.recursiveFlatFields,
                                                       this.recursiveFlatFields);

        // ability to change the default textContentProperty for one serialization run
        flags.textContentProperty = this._firstNonNull(dsRequest.textContentProperty,
                                                       operationBinding.textContentProperty);

        // serialization of DS metadata: "startRowTag" et al can be set on the operationBinding schema,
        // and that tag will take on the value of dsRequest.startRow
        flags.dsRequest = dsRequest;
        flags.startRowTag = operationBinding.startRowTag || this.startRowTag;
        flags.endRowTag = operationBinding.endRowTag || this.endRowTag;
        return flags;
    },

    // Serializing Objects as XML
    // ---------------------------------------------------------------------------------------
    // Overall: we go through declared subelements, where the value of each subelement is the
    // value of the same named property in the object being serialized.  For complexType
    // subelements (described by another DataSource), we recurse.
    // - flatData mode: we assume the inbound structure is an object with properties that hold
    //   values for same-named subelements at various levels in the structure.  This is for
    //   flattening structures that have gratuitous nesting and are reasonably editable in a
    //   single form.
    // - ordering issues: if the XML output should be something like:
    //   <page>
    //      <section ../>
    //      <header ../>
    //      <section ../>
    //   .. where a subelement name appears *twice* or more, we have no object representation
    //   for this.  One possibility here is a marker on a field that indicates it serializes
    //   it's contents without outputting a tag for the fieldName itself (this would be the
    //   same or a related marker to what would cause immediate Canvas-typed subelements to be
    //   automatically binned in Canvas.children by default).

    // NOTE: there are a few reasons why schema is a necessity in XML serialization:
    // - SOAP
    //   - namespace requirements - eg must be able to tell whether an element definition was
    //     declared at top-level in the WSDL file, which means it requires namespacing
    //     (regardless of whether it appears in a given message)
    //   - "encoded" messages where xsi:type attributes are required pervasively - no way to
    //     detect the appropriate type from live JS data
    // - XML Schema (including it's use in SOAP)
    //   - order dependencies - XML schema basically forces you to declare an order even if you
    //     don't want one, and that order must be followed or the message is considered invalid
    // - ISC Widget XML
    //   - type:multiple fields and ValueMaps have custom encoding

    //> @object SerializationContext
    // Flags for XML serialization
    // @treeLocation Client Reference/Data Binding/DataSource
    // @visibility external
    //<

    //> @attr serializationContext.useFlatFields (boolean : null : IR)
    // Enables flat serialization mode, as described for +link{dsRequest.useFlatFields}.
    // @visibility external
    //<

    //> @attr serializationContext.xmlExplicitType (boolean : null : IR)
    // If specified, include explicit type information using <code>xsi:type</code>
    // attributes on the generated nodes for any attributes where no explicit field type is
    // specified in the dataSource, or +link{dataSourceField.serializeXMLType} is set to true.
    // The type written out will be determined based on the specified field type (if present)
    // otherwise on the type of the data being serialized.
    // <P>
    // Note that a field type of <code>"any"</code> is treated the same as a field with
    // no specified type - the <code>xsi:type</code> flag will be generated by default, and
    // will have a value derived from the type of the JavaScript value being serialized.
    //
    // @visibility internal
    //<

    //> @attr serializationContext.indent (Boolean : null : IR)
    // If explicitly set to false whitespace indention is not included in serialization.
    //
    // @visibility internal
    //<

    //> @attr dataSourceField.serializeXMLType (boolean : null : IR)
    // Should an explicit "xsi:type" attribute be included in in the entry for this field
    // to indicate the data type when
    // +link{dataSource.xmlSerialize(),serializing data as XML}? Has no effect
    // unless +link{serializationContext.xmlExplicitType} is set to true.
    //
    // @visibility internal
    //<

    //> @method dataSource.xmlSerialize() [A]
    // Serialize a JavaScript object as XML.
    // <P>
    // The JavaScript Object passed to +link{method:DataSource.xmlSerialize()} becomes an XML
    // element named after the +link{attr:DataSource.tagName} (or +link{attr:DataSource.ID} if
    // tagName is unset).  Each property of the object becomes a subElement.  For example,
    // using a DataSource to serialize like this:
    // <pre>
    //     var inputObject = {
    //        startRow : 5,
    //        endRow : 50,
    //        data : [
    //           { field1 : "value1", field2: new Date() },
    //           { field1 : "value3", field2: null }
    //        ]
    //     };
    //     var myDS = isc.DataSource.create({ tagName:"DSRequest" });
    //     myDS.xmlSerialize(inputObject);
    // </pre>
    // .. produces the following XML:
    // <pre>
    //     &lt;DSRequest&gt;
    //         &lt;startRow&gt;5&lt;/startRow&gt;
    //         &lt;endRow&gt;50&lt;/endRow&gt;
    //         &lt;data&gt;
    //             &lt;field1&gt;value1&lt;/field1&gt;
    //             &lt;field2&gt;2005-10-14T18:01:16&lt;/field2&gt;
    //         &lt;/data&gt;
    //         &lt;data&gt;
    //             &lt;field1&gt;value3&lt;/field1&gt;
    //             &lt;field2&gt;&lt;/field2&gt;
    //         &lt;/data&gt;
    //     &lt;/DSRequest&gt;
    // </pre>
    // <P>
    // Various properties on the DataSource and DataSourceField can affect how serialization is
    // performed, see for example +link{DataSource.tagName},
    // +link{DataSource.schemaNamespace},
    // +link{DataSourceField.xmlAttribute},
    // +link{DataSourceField.multiple} and +link{DataSourceField.childTagName}.
    // By setting the +link{dataSourceField.type,type of a field} to the ID of another
    // DataSource which has further XML serialization settings, you can control serialization of
    // nested structures.
    // <P>
    // If you are working with a WSDL-described web service, XML serialization is performed
    // automatically by APIs like +link{method:WebService.callOperation()} - you only need to
    // know about serialization in order to understand how to put together JavaScript data that
    // will fill in an XML message properly, and for simple messages, setting
    // +link{dsRequest.useFlatFields} makes that unnecessary as well.
    // <P>
    // <b>Note:</b> when trying to send data to a web service, it is best to avoid putting
    // together any XML yourself, instead modify the JavaScript data being fed to SmartClient's
    // SOAP engine.  This is because the WSDL and SOAP rules for correctly namespacing and
    // encoding Web Service messages are very complex and are subject to change with new
    // versions of the web service you are contacting, whereas the data itself is easy to
    // manipulate and less likely to change.
    // <P>
    // To troubleshoot message formation, you can set the log category "xmlSerialize" to
    // <code>INFO</code> or <code>DEBUG</code> level in order to see diagnostics about XML
    // message formation, and you can use the RPC tab in the Developer Console to see the
    // actual messages being passed to web services.
    //
    // @param data (any) data to be serialized
    // @param flags (SerializationContext) options for the serialization engine
    // @return (String) data as serialized to XML
    //
    // @visibility xmlBinding
    //<
    xmlSerialize : function (data, flags, indent, tagName) {
        if (!flags) flags = {};

        if (flags.useFlatFields) flags.flatData = true;

        // qualifyAll, if set, should cascade to child elements which would not otherwise be
        // namespaced (because they were not declared as top level).  However when we are done
        // with this element, if we enabled qualifyAll we need to turn it back off
        var schemaSet = this.getSchemaSet(),
            enabledQualifyAll = (flags.qualifyAll == null);
        if (schemaSet && schemaSet.qualifyAll) {
            flags.qualifyAll = true;
        }


        var topLevel;
        if (flags.nsPrefixes == null) {
            var userPrefixes = this.xmlNamespaces ? isc.makeReverseMap(this.xmlNamespaces) : null;
            flags.nsPrefixes = isc.addProperties({}, userPrefixes);
            topLevel = true;
        }


        // enable xmlSchemaMode for any calls to xmlSerialize
        var oldXMLSchemaMode = isc.Comm.xmlSchemaMode;
        isc.Comm.xmlSchemaMode = true;
        // disable xsi:type unless it's been set explicitly



        var resetOmitXSI, omitXSIOldVal = null;
        if (flags.xmlExplicitType) {
            resetOmitXSI = true;
            omitXSIOldVal = isc.Comm.omitXSI;
            isc.Comm.omitXSI = false;
        } else {
            if (isc.Comm.omitXSI == null) {
                resetOmitXSI = isc.Comm.omitXSI = true;
            }
        }

        var result = this._xmlSerialize(data, flags, indent, tagName, topLevel);

        // reset qualify all
        if (enabledQualifyAll) flags.qualifyAll = null;

        // restore global xml serialize settings
        isc.Comm.xmlSchemaMode = oldXMLSchemaMode;
        if (resetOmitXSI) isc.Comm.omitXSI = omitXSIOldVal

        return result;
    },

    _xmlSerialize : function (data, flags, indent, tagName, topLevel) {
        if (isc._traceMarkers) arguments.__this = this;

        if (this.logIsDebugEnabled("xmlSerialize")) {
            this.logDebug("schema: " + this +
                         " serializing: " + this.echo(data) +
                         " with flags: " + this.echo(flags), "xmlSerialize");
        }

        var qualify = this.mustQualify || flags.qualifyAll,
            tagName = tagName || this.tagName || this.ID;


        if (!flags.ignoreConstructor) {
            var dataClassName;
            if (data != null && (data._constructor || isc.isAn.Instance(data))) {
                var dataClassName = isc.isAn.Instance(data) ? data.Class : data._constructor;
            }
        }

        // if we're passed an atomic value like a String, we definitely don't want to iterate
        // down a String's properties and pick up functions like "startsWith", the best we can
        // do is return the String inside our tags
        if (isc.DS.isSimpleTypeValue(data)) {

            if (isc.isA.String(data) && isc.startsWith(data,"ref:")) {
                return "<" + tagName + " ref=\"" + data.substring(4) + "\"/>";
            }

            this.logDebug("simple type value: " + this.echoLeaf(data) +
                          " passed to xmlSerialize on " + this, "xmlSerialize");
            return isc.Comm._xmlSerialize(tagName ||  this.tagName || this.ID, data);
        }

        // if it's an instance, also trim it down to serializable fields

        if (isc.isAn.Instance(data)) data = data.getSerializeableFields();

        if (isc.isAn.Array(data) && !this.canBeArrayValued) return this.map("xmlSerialize", data, flags, indent).join("\n");

        var output = isc.SB.create(),
            indent = indent || "";

        output.append("\r", indent);

        var schemaNamespace;
        if (qualify) {
            //this.logWarn("schema: " + this + " parentNS: " + flags.parentSchemaNamespace);
            // XML Schema namespacing rules: if a subelement declares its structure via a "ref"
            // attribute pointing to another *Element* definition, the namespace of the
            // referenced element must be used.  If the subelement declares its structure via a
            // "type" attribute, the namespace of the parent element must be used.
            schemaNamespace = (this.isA("XSComplexType") ? flags.parentSchemaNamespace : null)
                                    || this.schemaNamespace;
        }

        var xsiType;
        if (flags.xmlExplicitType) xsiType = null;
        else xsiType = this.ID;

        // leave the tag open..
        output.append(
            isc.Comm._xmlOpenTag(tagName, xsiType, schemaNamespace,
                                 flags.nsPrefixes, true)
        );

        // serialize attributes, if any.  Note only a field declared as an attribute
        // (field.xmlAttribute="true") ever becomes an attribute - undeclared data becomes
        // subelements
        data = this.serializeAttributes(data, output, flags);
        //this.logWarn("after serialize attributes, data is: " + this.echo(data));

        // if serializing an instance, save the actual class as "constructor" if it differs
        // from the schema we're using for serialization
        if (dataClassName && tagName != dataClassName) {
            if (isc[dataClassName] != null) {
                output.append(" constructor=\"", dataClassName, "\"");

            } else {
                output.append(" _constructor=\"", dataClassName, "\"");
            }
        }

        var subElements;
        if (data != null) {
            subElements = this.xmlSerializeFields(data, flags, indent+(!flags || flags.indent != false ? "    " : ""));
        }

        // if this is the top-level element, write out all the namespace prefixes encountered
        // during serialization
        if (topLevel) {
            output.append(this.outputNSPrefixes(flags.nsPrefixes, indent+(!flags || flags.indent != false ? "     " : ""), flags));
        }

        // NOTE: one time flag to prevent trailing ident, for use eg if there is text content
        var suppressIndent = this._suppressIndent;
        this._suppressIndent = null;

        // use a short tag if possible
        if (subElements == null || isc.isAn.emptyString(subElements)) {
            output.append("/>");
            return output.toString();
        }

        output.append(">", subElements,
                      // NOTE: one time flag to prevent if there is text content, don't add whitespace/newline inside tag
                      (suppressIndent ? "" : "\r" + indent));

        output.append(isc.Comm._xmlCloseTag(tagName, schemaNamespace, flags.nsPrefixes));

        return output.toString();
    },

    outputNSPrefixes : function (prefixes, indent, flags) {
        delete prefixes._nsCount;
        prefixes = isc.makeReverseMap(prefixes);
        var nsDefs = isc.xml._generateNamespaces(prefixes, null, indent+(!flags || flags.indent != false ? "        " : ""));
        return nsDefs;
    },

    serializeAttributes : function (data, output, flags) {
        var fieldNames = this.getFieldNames(),
            originalData = true;

        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = this.getField(fieldName);
            if (field.xmlAttribute && ((data && data[fieldName] != null) || field.xmlRequired)) {
                // if we run into attributes, shallow copy the data so we can remove already
                // serialized fields without affecting the data the developer passed to
                // xmlSerialize()
                if (originalData) {
                    data = isc.addProperties({}, data);
                    originalData = false;
                }

                var value = data[fieldName];
                if (flags && flags.spoofData) value = this.getSpoofedData(field);


                if (isc.isAn.Instance(value)) {
                    if (value.ID) {
                        value = value.ID;
                    } else {
                        this.logWarn("Serializing attribute " + fieldName + " failed to replace object instance with ID");
                    }
                }

                output.append(" ", fieldName, "=\"",
                        this._serializeSimpleTypeValue(field, value),
                        "\"");
                delete data[fieldName]
            }
        }
        return data; // return the (possibly copied) data
    },

    xmlSerializeFields : function (data, flags, indent) {
        var output = isc.SB.create(),
            flags = flags || isc.emptyObject,
            flatData = flags.flatData,
            spoofData = flags.spoofData,
            indent = indent || "";

        // output each known field *in order*
        var data = isc.addProperties({}, data); // copy data so we can delete known fields


        if (data.__ref) {
            delete data.__ref;
            delete data.__module;
        }

        var fields = this.getFields();
        for (var fieldName in fields) {
            var field = this.getField(fieldName),
                value = data[fieldName],

                fieldIsComplex = this.fieldIsComplexType(fieldName);

            var childData = data[fieldName];
            if (flags.startRowTag == field.name && childData == null) {
                childData = flags.dsRequest ? flags.dsRequest.startRow : null;
            } else if (flags.endRowTag == field.name && childData == null) {
                childData = flags.dsRequest ? flags.dsRequest.endRow : null;

            // flatData mode: we assume the inbound data is an object with properties
            // that hold values for same-named subelements at various levels in the
            // structure.  This is for flattening structures that have gratuitous nesting
            // and are reasonably editable in a single form.
            // Note that even with flatData=true, "data" may have nested objects, intended
            // as data for a same-named complexType subelement, so flatData amounts to always
            // passing data to child elements of complexType
            } else if (fieldIsComplex && flatData && childData == null) {
                childData = data;
            }

            //if (this.logIsDebugEnabled("xmlSerialize")) {
            //    this.logDebug("for field: '" + fieldName +
            //                 "' serializing data: " + this.echo(childData), "xmlSerialize");
            //}

            // NOTE: the xml notion of required is just that an element needs to be there - not
            // that it has to be non-empty
            var mustOutput = (field.xmlRequired && !field.xmlAttribute) ||
                                (data[fieldName] != null || (spoofData && !field.xmlAttribute));

            if (flatData && fieldIsComplex) {
                // HACK: for flatData, if a tag is not marked required, we can't easily
                // tell whether there will be output since subobjects may produce output if
                // the data happens to intersect with some simpleType field at any level of
                // nesting, so we just try serializing and throw the result away if there's
                // nothing
                var tagSchema = this.getSchema(field.type),
                    // HACK: this serialization run doesn't produce output, so disable caching
                    // of NS prefixes
                    origPrefixes = isc.clone(flags.nsPrefixes),
                    fieldsOutput = tagSchema.xmlSerializeFields(childData, flags);
                //this.logWarn("fieldName: '" + fieldName +
                //             "', mustOutput before flatData check: " + mustOutput +
                //             ", fieldsOutput: " + this.echoLeaf(fieldsOutput));
                if (fieldsOutput != null && !isc.isAn.emptyString(fieldsOutput)) {
                    mustOutput = true;
                }
                flags.nsPrefixes = origPrefixes;
            }

            if (mustOutput) {

                if (flatData && fieldIsComplex &&
                    // non-simpleType data is provided
                    data[fieldName] != null && !isc.DS.isSimpleTypeValue(data[fieldName]) &&
                    !flags.recursiveFlatFields)
                {
                    // NOTE: we intentionally addProperties() rather than clone() here, because
                    // we do want flags.nsPrefixes to have any additional nsPrefixes added to
                    // it
                    flags = isc.addProperties({}, flags);
                    flags.flatData = false;
                }
                output.append(this.xmlSerializeField(fieldName, childData, flags, indent));
            }
            // in flatData mode, we don't delete fields that we've output, because we don't
            // write out undeclared data at the end
            if (!flatData && data[fieldName] != null) delete data[fieldName];
        }

        // if there's any data left, tack them on the end, but *not* if this DataSource came
        // form XML Schema, in which case the extra data is sure to be invalid
        if (!flatData && !isc.isA.Schema(this)) {
            for (var fieldName in data) {
                output.append(this.xmlSerializeField(fieldName, data[fieldName], flags, indent));
            }
        }

        return output.toString();
    },

    _$Action:"Action",
    xmlSerializeField : function (fieldName, value, flags, indent) {
        var output = isc.SB.create(),
            field = this.getField(fieldName);
        // skip undeclared, generated properties
        if (field == null && (fieldName.startsWith("_") || fieldName.startsWith("$"))) return;

        var fieldType = (field ? field.type : null),
            flatData = flags && flags.flatData,
            spoofData = flags && flags.spoofData,
            indent = indent || "";

        if (spoofData) value = this.getSpoofedData(field);

        if (this.logIsDebugEnabled("xmlSerialize")) {
            this.logDebug("serializing fieldName: " + fieldName +
                          " with type: " + fieldType +
                          " with value: " + this.echo(value), "xmlSerialize");
        }

        // namespace the element if either everything must be qualifed or this particular field
        // is declared and must be qualified
        var namespace = ((field && field.mustQualify) || flags.qualifyAll ?
                         this.getSchemaSet().schemaNamespace : null);

        // if this field matches the textContentProperty, and either a textContent field has
        // been declared or there are no elements, return the value of the textContentProperty
        // with no surrounding tag and no indent.
        // NOTE: this handling appears here so that it works with flatData and spoofData
        var textContentProperty = flags.textContentProperty || this.textContentProperty,
            textContentField = this.getTextContentField();

        if (fieldName == textContentProperty &&
            (textContentField != null || !this.hasXMLElementFields(textContentProperty)))
        {
            // one time flag prevents normal whitespace/indent from being added
            this._suppressIndent = true;
            return this._serializeSimpleTypeValue(textContentField, value);
        }

        // special case for "Action"s - get the iscAction we stored on the function object
        if (fieldType == this._$Action && value != null) {
            if (value.iscAction) {
                value = value.iscAction
            // sanity check for StringMethods (may not be necessary since addMethods() /
            // addProperties() should have converted StringMethods to functions with iscAction
            // stored on them
            } else if (isc.isA.StringMethod(value)) {
                value = value.value;
            }
        }

        var xsiType = !flags.xmlExplicitType || this.shouldWriteSchemaType(field) ?
                    this._getXMLSchemaType(field ? field.type : null, value) : null;

        var fieldStart = isc.Comm._xmlOpenTag(fieldName, xsiType,
                                              namespace, flags.nsPrefixes),
            fieldEnd = isc.Comm._xmlCloseTag(fieldName, namespace, flags.nsPrefixes);

        //this.logWarn("schemaSet is: " + this.echo(schemaSet) + ", fieldStart: " + fieldStart);

        var values = isc.isAn.Array(value) ? value : [value];

        // If this DataSource has no field of the given name, see if we were passed another
        // DataSource to use as schema by the calling code.  RestDataSource uses this pattern
        // to allow it to control the overall style of XML serialization (the tag names to use
        // for criteria, criterion, etc) and the detailed serialization of individual fields
        // (most critically for fields of type "date", "time" and "datetime")
        if (!field && flags.schema && flags.schema.getField) {
            field = flags.schema.getField(fieldName);
        }

        // for a DataSource type, ask the DataSource to serialize the value
        if (this.fieldIsComplexType(fieldName)) {
            var oldPSN = flags.parentSchemaNamespace;
            flags.parentSchemaNamespace = this.schemaNamespace;
            //if (field && field.xsElementRef) this.logWarn("looking up *element* only: " + fieldType);

            var ds = this.getFieldDataSource(field, field && field.xsElementRef ? "element" : null);
            //this.logWarn("complexType field: " + fieldName +
            //             " with schema: " + ds +
            //             " has value: " + this.echo(value));

            if (field.multiple) {
                // if this is a field declaration with multiple=true, we write out a
                // container tag containing one or more tags for the values, where the values
                // have the typeName, eg
                //      <children> // container tag: fieldName
                //          <Canvas> // value tag: field dataSource
                //          </Canvas>
                //          <Canvas>
                //          </Canvas>
                //      </children>
                output.append("\r", indent, fieldStart);
                for (var i = 0; i < values.length; i++) {

                    output.append(ds.xmlSerialize(values[i], flags, indent+(!flags || flags.indent != false ? "    " : ""),
                                                  // allow the field to specify a name for
                                                  // child tags, eg, ds.fields -> "field"
                                                  values[i] == null
                                                   ? field.childTagName
                                                   : values[i]._tagName || field.childTagName));
                }
                output.append("\r", indent, fieldEnd);

            } else if (ds.canBeArrayValued && isc.isAn.Array(value)) {
                // very special case for the valueMap type, which when it appears as a simple
                // JS Array, shouldn't be considered multiple valueMaps.

                output.append(ds.xmlSerialize(value, flags, indent, fieldName));
            } else {
                // if this is a field that isn't declared multiple but has multiple values,
                // we write out one or more tags named after the field name since that's the
                // inbound format that would produce an array in a field not declared
                // multiple
                for (var i = 0; i < values.length; i++) {
                    var value = values[i];
                    if (value == null) { // null = empty tag
                        output.append("\r", indent)
                        output.append(fieldStart, fieldEnd);
                    // handle being unexpectedly passed a simple type.  If we're serializing a
                    // SOAP message, the resulting XML isn't going to adhere to the schema, but
                    // there's nothing we can do.
                    } else if (isc.DS.isSimpleTypeValue(value)) {
                        if (isc.isA.String(value) && isc.startsWith(value,"ref:")) {
                            output.append("\r", indent)
                            output.append(fieldStart);
                            var refTagName = (field ? field.childTagName || field.type : "value");
                            output.append("<", refTagName, " ref=\"", value.substring(4), "\"/>");
                            output.append(fieldEnd);
                        } else {
                            this.logWarn("simple type value " + this.echoLeaf(value) +
                                         " passed to complex field '" + field.name + "'",
                                         "xmlSerialize");
                            output.append("\r", indent)
                            output.append(isc.Comm.xmlSerialize(fieldName, value));
                        }
                    } else {
                        output.append(ds.xmlSerialize(value, flags, indent, fieldName));
                    }
                }
            }
            flags.parentSchemaNamespace = oldPSN;

        // declared field of simple type
        } else if (field != null) {

            // if this field came from a simple type element, we have to use the namespace that
            // the element was defined in.  Note that both xs:simpleTypes and xs:elements of
            // simpleType become SimpleTypes in SmartClient - both essentially represent a
            // reusable atomic type.
            if (field.xsElementRef) {
                var simpleType = this.getType(field.type);
                //this.logWarn("in DataSource: " + this +
                //             ", simpleType field: " + this.echo(field) +
                //             ", got simpleType: " + this.echo(simpleType));
                if (simpleType && simpleType.schemaNamespace)
                {
                    namespace = simpleType.schemaNamespace;
                }
            }

            if (field.multiple) {
                output.append("\r", indent, fieldStart, "\r");
                for (var i = 0; i < values.length; i++) {
                    // field.childTagName dictates the childTagName.  If it's null a generic
                    // name will be used
                    output.append(
                         this._serializeSimpleTypeTag(field.childTagName, field,
                                                      values[i], namespace, flags),
                         "\r", indent);
                }
                output.append("\r", indent, fieldEnd, "\r");
            } else {
                for (var i = 0; i < values.length; i++) {
                    output.append(
                        "\r", indent,
                        this._serializeSimpleTypeTag(fieldName, field,
                                                       values[i], namespace, flags));
                }
            }

        // undeclared field, simple or complexType
        } else {
            //this.logWarn("serializing values for undeclared field: " + fieldName +
            //             ", values: " + this.echoAll(values));
            for (var i = 0; i < values.length; i++) {
                if (values[i] == null || isc.isAn.emptyObject(values[i])) {
                    output.append("\r", indent, fieldStart, fieldEnd);
                } else {
                    output.append("\r", indent,
                                 isc.Comm._xmlSerialize(fieldName, values[i], indent,
                                                        { isRoot:false }));
                }
            }
        }

        return output.toString();
    },

    // When performing XML Serialization, should we include an "xsi:type" tag indicating
    // the type of data for some field.
    // This method is only called in the case where xmlSerialize was passed a context with
    // the "xmlExplicitType" flag set, and returns true for fields where we do want
    // an "xsi:type" entry - which is basically fields of variable type like the
    // 'any' field in a criterion object.
    shouldWriteSchemaType : function (field) {
        // No field - we don't have schema type info, so write out an explicit type based
        // on the value, so the parser will know how to regen JS on a round-trip.
        if (!field) return true;
        // Respect explicit "serializeXMLType" if set
        if (field.serializeXMLType != null) return field.serializeXMLType;
        else {
            // field has no specified type on the schema, so pick it up from the value.
            return (field.type == null || field.type == "any");
        }
    },


    _serializeSimpleTypeTag : function (fieldName, field, value, namespace, flags) {
        // NOTE: fieldName is passed as "null" for multiple:true fields, indicating use the
        // generic "value" for simple values, and let Comm.xmlSerialize pick a name if an
        // Object has been (erroneously) passed to a simple type field.  Comm.xmlSerialize will
        // use the Object's type as a tag name, defaulting to the generic "Object"

        var type = field.type,
            flags = flags || {};


        if (isc.isAn.Object(value) && !isc.isA.Function(value._xmlSerialize)) {
            return isc.Comm.xmlSerialize(fieldName || null, value);
        } else {


            var type = !flags.xmlExplicitType || this.shouldWriteSchemaType(field) ?
                        this._getXMLSchemaType(type, value) : null;

            // if the value is null and the field is nillable, send an empty element
            // (<element/>) with xsi:nil=true rather than an element with empty content
            // (<element></element>).  Note that this distinction is key for eg boolean or
            // other restricted types, where empty content is actually a bad value.
            if (value == null && field.nillable) {
                var tagName = fieldName || "value";
                return isc.Comm._xmlOpenTag(tagName, null, namespace, flags.nsPrefixes, true)
                        + " xsi:nil=\"true\"/>";
            }

            if (isc.isA.Date(value)) {
                // per XML Schema spec,
                // - type == "date" omits the time
                // - type == "time" omits the date and shows just the time
                // - type == "datetime" shows both.
                value = value.toSchemaDate(field.type, !!this.trimMilliseconds);
            } else if (value != null && value._xmlSerialize) {
                return value._xmlSerialize(fieldName, type, namespace);
            } else {
                // makeXMLSafe covers String, null (as empty string), Number and Boolean
                value = isc.makeXMLSafe(value);
            }
            return isc.Comm._xmlValue(fieldName || "value", value, type, namespace,
                                      flags.nsPrefixes);
        }
    },

    // serialize a simple type value without surrounding tags, for use in attributes and for
    // textContent fields.
    // If value is not actually of simple type, toString() is called on it (best we can do in a
    // context where we are not allowed to generate elements).
    // Pseudo-bug: the internal _xmlSerialize override is factored incorrectly because it
    // always returns a tag, so it cannot be called for an attribute value.
    _serializeSimpleTypeValue : function (field, value) {
        if (isc.isA.Date(value)) {
            return value.toSchemaDate(field ? field.type : null);
        } else {
            return isc.makeXMLSafe(value);
        }
    },

    // translate dataSource field types to their XML Schema equivalents, where we have synonyms
    _getXMLSchemaType : function (type, value) {
        // if a field has no type specified and we were passed a value, derive from
        // the value.

        if (value != null && (type == null || type == "any")) {
            if (value == null) return null;
            if (isc.isA.Date(value)) {
                return value.logicalDate ? "date"
                                         : (value.logicalTime ? "time" : "datetime");
            } else if (isc.isA.Number(value)) {
                if (parseInt(value) == value) {
                    return "float";
                } else {
                    return "integer";
                }
            } else if (isc.isA.Boolean(value)) {
                return "boolean";
            } else if (isc.isA.String(value)) {
                return "text";
            }
        }
        switch (type) {
            case "integer":
                return "int";
            case "number" :
                return "long";
            default:
                return type;
        }
    },

    xmlSerializeSample : function () {
        return this.xmlSerialize({}, { spoofData:true });
    },

    getSpoofedData : function (field) {
        if (!field) return "textValue";

        // return an object for object-type fields (to avoid following pathway of simpleType
        // passed to complexType field)
        if (this.getSchema(field.type) != null) return {};

        // return arrays for multiple fields
        if (field.multiple) {
            field = { type : field.type };
            return [this.getSpoofedData(field), this.getSpoofedData(field)];
        }

        // for valueMapped fields, return a value at random
        if (field.valueMap) {
            var values = !isc.isAn.Array(field.valueMap) ?
                    isc.getKeys(field.valueMap) : field.valueMap;
            return values[Math.round(Math.random() * (values.length-1))];
        }

        // return various simpleType values
        var baseType = isc.SimpleType.getBaseType(field.type);
        switch (baseType) {
            case "boolean" :
                return (Math.random() > 0.5);
            case "integer" :
            case "int" :
            case "number" :
                var min = 0, max = 10;
                if (field.validators) {
                    // check for validator._constructor as well as validator.type. This handles
                    // validators converted from Validator sub-object schema
                    var rangeValidator = field.validators.find("type", "integerRange") ||
                                         field.validators.find("_constructor", "integerRange");
                    if (rangeValidator) {
                        this.logWarn(field.name + " has " +
                                     rangeValidator.type + " validator " +
                                     " with min " + rangeValidator.min +
                                     " and max " + rangeValidator.max);
                        min = rangeValidator.min || 0;
                        // NOTE: this means if max is < 0 we always pick it exactly
                        max = rangeValidator.max || Math.min(min, 10);
                        if (min > max) min = max;
                    }
                }
                return Math.round(min + (Math.random() * (max-min)));
            case "float" :
            case "decimal" :
            case "double" :
                var min = 0, max = 10, precision = 2;
                if (field.validators) {
                    var rangeValidator = field.validators.find("type", "floatRange") ||
                                         field.validators.find("_constructor", "floatRange");
                    if (rangeValidator) {
                        this.logWarn(field.name + " has " +
                                     rangeValidator.type + " validator " +
                                     " with min " + rangeValidator.min +
                                     " and max " + rangeValidator.max);
                        min = rangeValidator.min || 0;
                        // NOTE: this means if max is < 0 we always pick it exactly
                        max = rangeValidator.max || Math.min(min, 10);
                        if (min > max) min = max;
                    }
                    var precisionValidator = field.validators.find("type", "floatPrecision") ||
                                             field.validators.find("_constructor", "floatPrecision");
                    if (precisionValidator) {
                        precision = precisionValidator.precision || 2;
                    }
                }
                return (min + (Math.random() * (max-min))).toFixed(precision);
            case "date" :
            case "time" :
            case "datetime" :
                var date = new Date();
                if (field.validators) {
                    var rangeValidator = field.validators.find("type", "dateRange") ||
                                         field.validators.find("_constructor", "dateRange");
                    if (rangeValidator) {
                        this.logWarn(field.name + " has " +
                                     rangeValidator.type + " validator " +
                                     " with min " + rangeValidator.min +
                                     " and max " + rangeValidator.max);
                        // NOTE: picking a random date between min and max would require at
                        // least zeroing out either time or date values for pure date or pure
                        // time value fields
                        if (rangeValidator.min) date = rangeValidator.min;
                        else if (rangeValidator.max) date = rangeValidator.max;
                    }
                }
                return date;
            default:

                return "textValue";
        }
    },


    //>EditMode
    // extract saveable data from a live DataSource
    getSerializeableFields : function (removeFields, keepFields) {
        var props = this.Super("getSerializeableFields", arguments);

        var fields = props.fields;
        // switch back from Map to Array
        fields = isc.getValues(fields);
        // remove type-based validators automatically inserted into validators Array
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i] = isc.addProperties({}, fields[i]);
            var validators = field.validators;
            if (validators) {
                field.validators = validators.findAll("_generated", null);
                if (field.validators == null) delete field.validators;
            }
        }
        props.fields = fields;

        return props;
    },
    //<EditMode

    // XML Response handling
    // ---------------------------------------------------------------------------------------

    // take the XML response from a web service and ask the DataSource to provide an Array of
    // XMLElements from it
    _handleXMLReply : function (xmlDoc, xmlText, rpcResponse, rpcRequest) {
        var xmlData = xmlDoc,
            dsRequest = rpcRequest._dsRequest,
            operationBinding = this.getOperationBinding(dsRequest),
            opNamespaces;

        // Don't try to handle the response unless it's valid
        if (rpcResponse.status < 0) {
            var data = xmlText || rpcResponse.data;
            this._completeResponseProcessing(data, {status: rpcResponse.status, data:data},
                                             dsRequest,
                                             rpcResponse, rpcRequest);
            return;
        }

        // add namespaces to the xmlDoc to make subsequent selection easier
        if (xmlData) {
            if (operationBinding.wsOperation) {
                var service = this.getWebService(dsRequest),
                    opNamespaces = service.getOutputNamespaces(operationBinding.wsOperation);
                xmlData.addNamespaces(opNamespaces);
            }
            xmlData.addNamespaces(this.xmlNamespaces);
            xmlData.addNamespaces(operationBinding.xmlNamespaces);
        }

        // Ensure the full set of defined namespaces is available to all xPaths
        // executed during dsResponseFromXML.  Currently we associate all namespaces with the
        // XMLDoc, which means they'll be available for selectNodes() called directly with the
        // xmlDoc, but not for selectNodes() relative to an element (eg valueXPath).
        // To fix this pervasively such that any call to selectNodes() with an element works,
        // we should either tag or wrap all native XMLElements returned from selectNodes() so
        // that we always find the right set of namespaces.
        var xmlNamespaces = isc.addProperties({}, opNamespaces,
                                               this.xmlNamespaces,
                                               operationBinding.xmlNamespaces);

        // NOTE: it's key to pass the dsRequest to dsResponseToXML since among other things
        // custom message formats may have the server just return a range of rows without
        // reminding the client what the range is
        // dsResponseFromXML can be asynchronous (depending on resultBatchSize)
        this.dsResponseFromXML(xmlData, dsRequest, xmlNamespaces,
                                {target:this, methodName:"_completeHandleXMLReply",
                                    xmlData:xmlData, dsRequest:dsRequest,
                                    rpcRequest:rpcRequest, rpcResponse:rpcResponse
                                }
        );

    },

    _completeHandleXMLReply : function (dsResponse, callback) {
        this._completeResponseProcessing(callback.xmlData, dsResponse, callback.dsRequest,
                                                  callback.rpcResponse, callback.rpcRequest);
    },

    //> @method DataSource.dsResponseFromXML() [A]
    // Generate a DSResponse from an XML Document already present on the client.
    // Makes use of <code>recordXPath</code> specified on the DataSource or the appropriate
    // operationBinding to select the appropriate set of nodes, and extracts record data
    // as described in +link{DataSource.recordsFromXML()}. Extracted data is then assembled
    // into a <code>DSResponse</code> object, available on the callback.
    // @param xmlData (XML Document) XML Data from with the DSResponse will be generated
    // @param dsRequest (DSRequest) Request object
    // @param xmlNamespaces (object) Namespace prefixes to apply when extracting data
    // @param callback (Callback) callback to fire when the response has been generated. Takes
    //  a single parameter <code>dsResponse</code> - the generated response object.
    //
    // @visibility internal
    //<

    dsResponseFromXML : function (xmlData, dsRequest, xmlNamespaces, callback) {
        if (xmlData) {
            this.selectRecords(
                xmlData, dsRequest,
                {target:this, methodName:"_completeDSResponseFromXML", dsRequest:dsRequest,
                 callback:callback, xmlData:xmlData, xmlNamespaces:xmlNamespaces}
            );
        } else {
            this._completeDSResponseFromXML([], dsRequest, xmlNamespaces, callback);
        }
    },

    _completeDSResponseFromXML : function (data, dsRequest, xmlNamespaces, callback) {
        // handle the case where this is fired asynchronously as a callback -
        // pick up the dsRequest from the 2nd arguments (the callback object)
        if (!callback && dsRequest.callback) callback = dsRequest.callback;
        if (dsRequest.xmlNamespaces) xmlNamespaces = dsRequest.xmlNamespaces;
        if (dsRequest.dsRequest) dsRequest = dsRequest.dsRequest;
        if (xmlNamespaces == null) xmlNamespaces = this.xmlNamespaces;
        // default startRow as matching what was requested
        var dsResponse = {
            data : data,
            startRow: dsRequest.startRow || 0,
            status : 0
        };
        dsResponse.endRow = dsResponse.startRow + Math.max(0, data.length);
        // assume dataset is complete by default
        dsResponse.totalRows = Math.max(dsResponse.endRow, data.length);
        var xmlData = callback.xmlData;

        // added null check on xmlData for the empty response case
        if (xmlData) {
            if (this.totalRowsXPath) {
                dsResponse.totalRows =
                    isc.xml.selectNumber(xmlData, this.totalRowsXPath, xmlNamespaces, true);
            }
            if (this.startRowXPath) {
                dsResponse.startRow =
                    isc.xml.selectNumber(xmlData, this.startRowXPath, xmlNamespaces, true);
                dsResponse.endRow = dsResponse.startRow + Math.max(0, data.length);
            }
            if (this.endRowXPath) {
                dsResponse.endRow =
                    isc.xml.selectNumber(xmlData, this.endRowXPath, xmlNamespaces, true);
                // if there's no XPath specified for the startRow, derive it from endRow - number
                // of records returned
                if (!this.startRowXPath) {
                    dsResponse.startRow = dsResponse.endRow - Math.max(0, data.length);
                }
            }

            // an XPath is unlikely to be able to really translate 3rd party error codes to ISC
            // error codes, but might allow a first cut of error vs no error
            if (this.statusXPath) {
                dsResponse.status = parseInt(
                    isc.xml.selectScalar(xmlData, this.statusXPath, xmlNamespaces, true));
            }
            if (this.errorSchema) {
                dsResponse.errors = this.errorSchema.selectRecords(xmlData, dsRequest);
            }
        }
        if (callback) this.fireCallback(callback, "dsResponse", [dsResponse, callback])
        return dsResponse;
    },



    //>@attr dataSource.resultBatchSize (int : 150 : IRWA)
    // Very advanced: for servers that do not support paging, and must return large numbers of XML
    // records in one HTTP response, SmartClient breaks up the processing of the response
    // in order to avoid the "script running slowly" dialog appearing for an end user.
    // <P>
    // If you have a relatively small number of records with a great deal of properties or
    // subobjects on each record, and you have not set +link{dropExtraFields} to eliminate unused
    // data, and you see the "script running slowly" dialog, you may need to set this number
    // lower.
    // @serverDS allowed
    // @visibility external
    //<

    resultBatchSize:150,

    // extract records from an XML document using the recordXPath, and transform to JS using
    // the schema
    // @visibility xmlBinding

    selectRecords : function (xmlData, dsRequest, callback) {
        var elements = this.selectRecordElements(xmlData, dsRequest);
        var operationBinding = this.getOperationBinding(dsRequest),
            schema = this.getSchema(operationBinding.responseDataSchema) || this;
        return schema.recordsFromXML(elements, callback);
    },

    //> @method dataSource.recordsFromXML() [A]
    // Transform a list of XML elements to DataSource records.
    // <P>
    // <code>recordsFromXML()</code> will create a List of DataSource records in the form of
    // JavaScript objects.  The value for each field is extracted from the XML according to the
    // rules described under +link{attr:DataSourceField.valueXPath}.
    // <P>
    // Derived JavaScript values for each field will be the appropriate JavaScript type, eg,
    // for a field of "date" type, the JS value will be a JavaScript Date object.
    // <P>
    // Note that if the set of nodes exceeds +link{DataSource.resultBatchSize}, this method
    // will break the XML processing logic into multiple threads and therefore complete
    // asynchronously. In this case the return value will be null, and the callback parameter
    // must be used to process the extracted records.
    //
    // @param elements (List of XMLElement) XML elements to transform, eg, the result of a
    //                                       call to +link{XMLTools.selectNodes()}
    // @param callback (Callback) Callback to fire when the transform completes. Takes a single
    //    parameter <code>records</code> - the array of DataSource records derived from the
    //    XML elements.
    //
    // @return (List) List of DataSource records derived from the XML elements, or null
    //   if this method completed the XML processing asynchronously.
    // @visibility xmlBinding
    //<
    recordsFromXML : function (elements, callback) {
        // handle being passed a single element, or a native nodeList (as returned by natively
        // calling selectNodes)
        // check elements and skip if null (empty response case)
        if (elements && !isc.isAn.Array(elements)) {
            // XML elements have no length attribute, but NodeLists do
            if (elements.length != null) elements = isc.xml._nodeListToArray(elements);
            // assume a single XML element
            else elements = [elements];
        }

        if (elements && this.transformResponseToJS) {
            if (elements.length > this.resultBatchSize) {
                var fromXMLContext = {startingRow:0, callback:callback, elements:elements};
                return this._asyncRecordsFromXML(fromXMLContext);

            }

            var attrMask = this.dropExtraFields ? this.getFieldNames() : null;

            elements = isc.xml.toJS(elements, attrMask, this);
            if (this.logIsDebugEnabled("xmlBinding")) {
                this.logDebug("transformed response: " + this.echoFull(elements) +
                              "xmlBinding");
            }
        }

        if (callback) {
            this.fireCallback(callback, "records", [elements, callback]);
        }
        return elements;
    },

    _asyncRecordsFromXML : function (context) {

        var elements = context.elements,
            startingRow = context.startingRow,
            callback = context.callback,
            endRow = Math.min(elements.length, startingRow + this.resultBatchSize),
            attrMask = this.dropExtraFields ? this.getFieldNames() : null;

        if (!context._recordsFromXML) {
            context._recordsFromXML = isc.xml.toJS(
                                        elements.slice(startingRow, endRow+1), attrMask, this);
        } else {
            var records = isc.xml.toJS(elements.slice(startingRow, endRow+1), attrMask, this);
            context._recordsFromXML.addList(records);
        }
        if (endRow < elements.length) {
            context.startingRow = endRow + 1;
            this.delayCall("_asyncRecordsFromXML", [context]);
        } else if (callback) {
            this.fireCallback(callback, "records", [context._recordsFromXML, callback]);
        }
    },

    // given xmlData return by a web service, return an Array of XML elements to be used as a
    // recordSet
    selectRecordElements : function (xmlData, dsRequest) {
        if (isc.isA.String(xmlData)) xmlData = isc.xml.parseXML(xmlData);


        // get the XPath or tagName to use to select record elements from the operationBinding
        var operationBinding = this.getOperationBinding(dsRequest);

        // precedence order for selecting records: opBinding.recordXPath, opBinding.recordName,
        //                                         dataSource.recordXPath, dataSource.recordName
        // Slightly tricky because operationBinding defaults to the DataSource itself when
        // there is no operationBinding for the operation.
        var opXPath = operationBinding == this ? null : operationBinding.recordXPath,
            opRecordName = operationBinding == this ? null : operationBinding.recordName,
            dsXPath = this.recordXPath,
            dsRecordName = this.recordName;

        // if we have a recordName and a WSDL service definition, and there is no overriding
        // recordXPath definition, use the WebService to select the records by type.
        if (opXPath == null &&
            (opRecordName != null || (dsXPath == null && dsRecordName != null)) &&
            this.hasWSDLService(dsRequest))
        {
            // if we have a WebService, allow the recordName to be used as a way of
            // auto-generating the recordXPath
            var service = this.getWebService(dsRequest);

            return service.selectByType(xmlData,
                                        operationBinding.wsOperation || this.wsOperation,
                                        opRecordName || dsRecordName);
        }

        // use a recordXPath if specified, otherwise use recordName to do just a simple
        // getElementsByTagName
        var recordXPath = opXPath || dsXPath,
            elements;
        if (recordXPath) {
            elements = isc.xml.selectNodes(xmlData, recordXPath, this.xmlNamespaces);
            this.logDebug("applying XPath: " + recordXPath +
                          (this.xmlNamespaces ?
                           " with namespaces: " + this.echo(this.xmlNamespaces) : "") +
                          " got " + (elements ? elements.length : null) + " elements", "xmlBinding");
        } else {
            elements = [];
            var recordName = opRecordName || dsRecordName || this.ID;
            // NOTE: a tagName of "record" matches <foo:record> in Moz, but NOT in IE
            var nodeList = xmlData.getElementsByTagName(recordName);
            for (var i = 0; i < nodeList.length; i++) elements.add(nodeList[i]);
            this.logDebug("getting elements of tag name: " + recordName +
                          " got " + elements.length + " elements", "xmlBinding");
        }

        return elements;
    },

    // Common XML / JSON Response handling
    // ---------------------------------------------------------------------------------------

    _completeResponseProcessing : function (data, dsResponse, dsRequest,
                                                  rpcResponse, rpcRequest)
    {
        // empty response case, set up dsResponse manually
       if (!dsResponse) {

           // set the status and httpResponseCode, or the saving of local data will fail
            dsResponse = {
                status: rpcResponse.status,
                httpResponseCode: rpcResponse.httpResponseCode
            };
        }
        // note for "clientCustom" operations this method is fired directly from
        // processResponse() and rpcResponse / rpcRequest will be null
        if (rpcResponse != null && rpcRequest != null) {
            // make the httpResponseCode available on dsResponse - needed for REST implementations
            dsResponse.httpResponseCode = rpcResponse.httpResponseCode;
            dsResponse.httpResponseText = rpcResponse.httpResponseText;
            // grab the transactionNum - needed for relogin via resubmitTransaction()
            dsResponse.transactionNum = rpcResponse.transactionNum;
            // place the clientContext on the response
            dsResponse.clientContext = rpcRequest.clientContext;
            dsResponse.internalClientContext = rpcRequest.internalClientContext;
            dsResponse.httpHeaders = rpcResponse.httpHeaders;
        } else {
            dsResponse.clientContext = dsRequest.clientContext;
            dsResponse.internalClientContext = dsRequest.internalClientContext;
        }


        if (this.logIsInfoEnabled("xmlBinding")) {
            this.logInfo("dsResponse is: " + this.echo(dsResponse), "xmlBinding");
        }


        dsResponse.context = rpcRequest;

        // allow customization
        var modifiedResponse = this.transformResponse(dsResponse, dsRequest, data);
        // handle failing to return the response - assume it's been modified in place
        dsResponse = modifiedResponse || dsResponse;
        // convert startRow, endRow and totalRows from string to integer if necessary, and
        // provide defaults if the values are senseless
        dsResponse.startRow = this._parseNumber(dsResponse.startRow, 0);
        var endRow = dsResponse.endRow;
        if (endRow == null) {
            if (dsResponse.status < 0) endRow = 0;
            else if (isc.isAn.Array(dsResponse.data)) endRow = dsResponse.data.length;
            else endRow = 1;
        }

        dsResponse.endRow = this._parseNumber(endRow);
        dsResponse.totalRows = this._parseNumber(dsResponse.totalRows, dsResponse.endRow);

        // if cacheAllData:false and autoCacheAllData:true and this fetch response includes
        // all posible records, switch cacheAllData on and proceed
        // Note 'downloadResult' requests bypass the cache-data so don't allow these to toggle
        // this.cacheAllData
        if (!this.cacheAllData && this.autoCacheAllData &&
            dsRequest.downloadResult != true &&
            dsResponse.operationType == "fetch" &&
            (dsRequest.data == null || isc.isAn.emptyObject(dsRequest.data)) &&
            dsResponse.startRow == 0 && dsResponse.endRow == dsResponse.totalRows)
        {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("Response: switching on cacheAllData", "cacheAllData");
            }
            this.cacheAllData = true;
            this._autoCacheAllData_timestamp = isc.timestamp();
        }

        //>Offline cache responses for subsequent offline use.  Do this after the response is
        // known to be data-provider agnostic, that is, dsResponse.data is an Array of Object
        // and not any provider-specific format.  This allows custom data protocols to use the
        // offline system transparently, and skips the maximum amount of work when using
        // offline response (eg, no parsing WSDL responses again)
        if (this.useOfflineStorage && dsResponse.status == 0 && !this.clientOnly) {
            isc.DataSource.cacheResponse(dsRequest, dsResponse);
            // It's a waste of time to re-stash a response that came from Offline (and it also
            // leads to a misleading timestamp on the response - it effectively becomes a
            // last-accessed timestamp rather than a stored timestamp)
            if (isc.Offline && !dsResponse.fromOfflineCache) {
                if (dsRequest.unconvertedDSRequest) {
                    isc.Offline.storeResponse(dsRequest.unconvertedDSRequest, dsResponse);
                } else {
                    isc.Offline.storeResponse(dsRequest, dsResponse);
                }
            }
        }

        // HACK: used by DBCs to tell if their data is in offline mode
        if (dsRequest && dsRequest.resultSet) {
            if (dsResponse.status == isc.RPCResponse.STATUS_OFFLINE) {
                dsRequest.resultSet._offline = true;
            } else {
                dsRequest.resultSet._offline = false;
            }
        } else if (dsRequest && dsRequest.resultTree) {
            if (dsResponse.status == isc.RPCResponse.STATUS_OFFLINE) {
                dsRequest.resultTree._offline = true;
            } else {
                dsRequest.resultTree._offline = false;
            }
        }
        //<Offline

        this.fireResponseCallbacks(dsResponse, dsRequest, rpcResponse, rpcRequest);

        if (dsResponse.relatedUpdates != null) {
            for (var i = 0; i < dsResponse.relatedUpdates.length; i++) {
                if (dsResponse.relatedUpdates[i].operationType == null) {
                    dsResponse.relatedUpdates[i].operationType = dsResponse.operationType;
                }

                isc.DS.get(dsResponse.relatedUpdates[i].dataSource).updateCaches(dsResponse.relatedUpdates[i], null);
            }
        }

    },

    fireResponseCallbacks : function (dsResponse, dsRequest, rpcResponse, rpcRequest) {

        if (!dsResponse.clientContext) dsResponse.clientContext = {};

        if (dsResponse.status >= 0) {
            // on success, notify ResultSets
            isc.DataSource.handleUpdate(dsResponse, dsRequest);
        } else if (!dsRequest.willHandleError) {
            isc.RPCManager._handleError(dsResponse, dsRequest);
        }

        // fire callbacks:
                        // passed directly to a DataSource method, eg ds.fetchData()
        var callbacks = [dsRequest._dsCallback,
                        // secondary convenience callback, see ActionMethods::buildRequest()
                         dsRequest.afterFlowCallback],
            firedCallbacks = [];

        for (var i = 0; i < callbacks.length; i++) {

            var callback = callbacks[i];

            // paranoid duplicate callback check
            if (firedCallbacks.contains(callback)) {
                this.logWarn("Suppressed duplicate callback: " + callback);
                continue;
            }

            var callbackResult = this.fireCallback(callback, "dsResponse,data,dsRequest",
                                                    [dsResponse,dsResponse.data,dsRequest]);


            // the primary callback, which completes the basic dataSource flow, can stop
            // further processing by returning false.  This is intended to allow flow code to
            // signal fundamental errors that high-level user code isn't expected to handle.
            if (rpcRequest && rpcRequest.willHandleError && callbackResult === false) {
                //>DEBUG
                this.logDebug("performOperationReply: Further processing cancelled by callback");
                //<DEBUG
                break;
            }

            // If transaction has been suspended, stop processing callbacks
            // Can happen on EG login failure

            if (rpcResponse) {
                var transaction = isc.RPCManager.getTransaction(rpcResponse.transactionNum);
                if (transaction && transaction.suspended) return;
            }
        }

    },

    // if argument is a string, try parsing it as a number and return parsed value as long as
    // it's not NaN
    _parseNumber : function (number, defaultValue) {
        if (number == null) return defaultValue;
        if (!isc.isA.String(number)) return number;
        var parsedNum = parseInt(number);
        if (isNaN(parsedNum)) return defaultValue != null ? defaultValue : number;
        else return parsedNum;
    },

    //> @method dataSource.transformResponse() [A]
    // Modify the DSResponse object derived from the response returned from the
    // +link{dataSource.dataURL,dataURL}.
    // <P>
    // This is an override point that makes it possible to use DataSource features such as
    // paging with web services that support such features, by allowing you to fill in metadata
    // fields in the DSResponse object (such as +link{attr:DSResponse.startRow}) based on
    // service-specific metadata fields contained in the service's response.
    // <P>
    // The DSResponse passed to this method already has +link{attr:dsResponse.data}, which is
    // derived differently depending on the +link{dataSource.dataFormat,dataFormat} setting:
    // <ul>
    // <li> <code>dataFormat:"xml"</code> : either the
    // +link{attr:operationBinding.recordXPath,recordXPath} or
    // +link{attr:operationBinding.recordName,recordName} is used to select the XML elements
    // that represent DataSource records.  The selected XML elements are passed to
    // +link{method:DataSource.recordsFromXML()}, which transforms the XML elements to typed
    // JavaScript data using the DataSource as a schema.
    // <li> <code>dataFormat:"json"</code> : the
    // +link{attr:operationBinding.recordXPath,recordXPath}, if specified, is used to select
    // records from the returned JSON data via +link{XMLTools.selectObjects()}.
    // +link{DataSourceField.valueXPath} is used to derive correctly typed field values.
    // <li> <code>dataFormat:"custom"</code> : <code>dsResponse.data</code> is the raw response
    // in String form.  It must be parsed into an Array of Objects for subsequent processing to
    // work.
    // </ul>
    // <P>
    // In addition to <code>dsResponse.data</code>, +link{attr:dsResponse.status} is defaulted
    // to 0 (indicating no error), and +link{attr:dsResponse.startRow} is assumed to be zero,
    // with +link{attr:dsResponse.endRow,endRow} and +link{attr:dsResponse.totalRows,totalRows}
    // both set to <code>dsResponse.data.length - 1</code>, that is, the returned data is
    // assumed to be all records that matched the filter criteria.
    // <P>
    // Examples of using this API include:
    // <ul>
    // <li> setting +link{attr:dsResponse.startRow,startRow},
    //      +link{attr:dsResponse.endRow,endRow} and +link{attr:dsResponse.totalRows,totalRows}
    //      to allow paging for a service that supports it.  For example, if an XML service
    //      returns a "resultRow" tag that contained the row number of the first row of the
    //      returned results:<pre>
    //    dsResponse.startRow =
    //        isc.XMLTools.selectNumber(xmlData, "//resultRow");
    // </pre>
    // <li> setting +link{attr:dsResponse.status} to recognized ISC error values based on
    //      service-specific errors, in order to trigger standard ISC error handling.  For
    //      example, setting <code>dsResponse.status</code> to
    //      +link{rpcResponse.STATUS_VALIDATION_ERROR} and filling in
    //      +link{attr:dsResponse.errors} in order to cause validation errors to be shown in
    //      forms and grids.
    // <li> for services that either do not return cache update data, or return partial data,
    //      using +link{attr:dsRequest.oldValues} to create cache update data (whether this is
    //      appropriate is application-specific), or setting
    //      +link{attr:dsResponse.invalidateCache}.
    // </ul>
    // <P>
    // <span STYLE="color:red">NOTE:</span> this method is NOT an appropriate time to call
    // methods on visual components such as grids, initiate new DSRequests or RPCRequests, or
    // in general do anything other than fill in fields on the DSResponse based on data that is
    // already available.  Any actions that need to be taken as a result of the web
    // service response should be implemented exactly as for a DataSource where
    // <code>transformResponse()</code> has not been overridden, that is, use the callback
    // passed to high-level methods such as
    // +link{group:dataBoundComponentMethods,<code>grid.fetchData()</code>}, and do error
    // handling via either +link{DataSource.handleError()} or by setting
    // +link{RPCRequest.willHandleError,willHandleError}.
    //
    // @param dsResponse (DSResponse) default DSResponse derived from the response data
    // @param dsRequest (DSRequest) DSRequest object that initiated this request
    // @param data (XMLDocument or JSON) XML document or JSON objects returned by the web
    //                                   service
    // @return (DSResponse) response derived
    // @visibility xmlBinding
    // @example xmlServerValidationErrors
    //<

    transformResponse : function (dsResponse, dsRequest, data) {
        return dsResponse;
    },

    // get the value of a field of a record from this DataSource.  Used for dynamically
    // fetching field values from XML elements
    getFieldValue : function (record, fieldName, field) {
        var value = isc.xml.getFieldValue(record, fieldName, field, this, this.xmlNamespaces);
        if (!field.getFieldValue) return value;

        if (!isc.isA.Function(field.getFieldValue)) {
            isc.Func.replaceWithMethod(field, "getFieldValue", "record,value,field,fieldName");
        }

        return field.getFieldValue(record, value, field, fieldName);
    },

    _singleArray : [],
    // validate a value for a field of simple type
    validateFieldValue : function (field, value) {
        if (!isc.Validator) return value; // forms module not loaded

        var validators = field.validators;
        if (!validators) return value;

        // normalize to an Array if necessary, avoiding an allocation
        if (!isc.isAn.Array(validators)) {
            this._singleArray[0] = validators;
            validators = this._singleArray;
        }

        var origValue = value; // purely for log below

        for (var i = 0; i < validators.length; i++) {
            var validator = validators[i];
            var result = isc.Validator.processValidator(field, validator, value, null, null);
            if (!result) {
                this.logWarn(this.ID + "." + field.name +
                             ": value: " + this.echoLeaf(value) +
                             " failed on validator: " + this.echo(validator));
                // NOTE: the value we return is the value as transformed by what validators did
                // not fail.  Hence, having originally been a String, it may now be an integer,
                // but not in the correct range.
                return value;
            }
            var undef;
            if (validator.resultingValue !== undef) {
                value = validator.resultingValue;
                validator.resultingValue = undef;
            }
            // if the validator wants us to stop if it wasn't passed, break out of the
            // validators loop
            if (!result && validator.stopIfFalse) break;
        }

        //this.logWarn("At field: " + this.ID + "." + field.name +
        //             " validating value: " + this.echoLeaf(origValue) +
        //             " with validators: " + this.echo(validators) +
        //             " produced result: " + this.echo(value));

        this._singleArray.length = 0;
        return value;
    },

    // Give a standard or advanced criteria object, return the complete set of
    // field names referenced.
    getCriteriaFields : function (criteria) {
        return isc.DS.getCriteriaFields(criteria, this);
    },

    // Drill into advancedCriteria object extracting fields
    _getAdvancedCriteriaFields : function (criterion, fieldNames) {
        return isc.DS._getAdvancedCriteriaFields(criterion, fieldNames);
    },

    // DataSource operation methods
    // --------------------------------------------------------------------------------------------

    // NOTE: concurrent editing:
    //   - for updates and deletes we'll need the oldValues to detect changes in the target
    //     record
    //   - oldValues will be passed as requestProperties.oldValues
    //   - oldValues will remain optional since many datasets are single-user
    //     - NOTE however: even single user web applications can be logged into simultaneously
    //       from two locations (eg email)

    //> @method dataSource.fetchRecord()
    // Fetch a single record from the DataSource by +link{dataSourceField.primaryKey,primary key}.
    // This simply calls +link{fetchData()} after creating +link{Criteria} that contain the primary
    // key field and value.
    // <p>
    // If you call this method on a DataSource with a composite primary key - ie, one with
    // multiple primaryKey fields - this method returns the first record where the first
    // defined primary field matches the supplied pkValue; this may or may not be meaningful,
    // depending on your use case.  Generally, for DataSources with composite keys, it makes
    // more sense to use <code>fetchData()</code> directly, rather than this convenience
    // method.
    //
    // @param pkValue (any) value for the field marked
    //                      +link{dataSourceField.primaryKey,primaryKey}:true in this DataSource
    //                      (or the first field so marked if there is more than one)
    // @param [callback] (DSCallback)  callback to invoke on completion
    // @param [requestProperties] (DSRequest Properties) additional properties to set on the
    //                                                   DSRequest that will be issued
    // @visibility external
    //<
    fetchRecord : function (pkValue, callback, requestProperties) {
        var criteria = {},
            pkField = this.getPrimaryKeyField();
        if (pkField == null) {
            this.logWarn("This datasource has no primary key field. Ignoring fetchRecord call");
            return;
        }
        var pkFieldName = pkField.name;

        // handle being passed criteria rather than a single pkField value
        var undefined;
        if (isc.isAn.Object(pkValue) && pkValue[pkFieldName] !== undefined) {
            criteria = pkValue
        } else {
            criteria[pkFieldName] = pkValue
        }
        return this.fetchData(criteria, callback, requestProperties);
    },



    //> @class Callbacks
    // This object cannot be used; it exists for documentation purposes only as a place to put
    // documentation for callback methods, such as the callback for +link{Callbacks.DSCallback,DataSource.fetchData()}.
    //
    // @treeLocation Client Reference/Data Binding
    // @visibility external
    //<

    //> @method Callbacks.DSCallback
    // Callback fired when DataSource methods that send DSRequests complete (such as +link{DataSource.fetchData()}).
    //
    // @param dsResponse (DSResponse) a +link{class:DSResponse} instance with metadata about the returned data
    // @param data (any) data returned to satisfy the DataSource request. See the
    //        +link{group:dataSourceOperations,DataSource operations} topic for expected results
    //        for each type of DataSource operation
    // @param dsRequest (DSRequest) the +link{class:DSRequest} that was sent. You can use
    //        +link{attr:dsRequest.clientContext} to track state during the server turnaround.
    //
    // @visibility external
    //<

    //> @method dataSource.fetchData()
    // Perform a "fetch" DataSource operation against this DataSource, sending search criteria
    // and retrieving matching records.
    // <P>
    // <b>NOTE:</b> do not attempt to override this method to create a custom DataSource.  For
    // a server-side custom DataSource, use the +link{DataSource.serverConstructor} attribute,
    // and the +explorerExample{customDataSource,Custom DataSource samples}.  For a
    // client-side custom DataSource, see +link{dataProtocol,dataProtocol:"custom"}.
    // <P>
    // In contrast to +link{listGrid.fetchData()}, which creates a +link{ResultSet} to manage
    // the returned data, calling <code>dataSource.fetchData()</code> provides the returned
    // data in the callback as a
    // <smartclient>simple JavaScript Array of JavaScript Objects.</smartclient>
    // <smartgwt>RecordList or simple Array of Record objects.</smartgwt>  Calling
    // <code>dataSource.fetchData()</code> does not automatically update any visual components or
    // caches: code in the callback passed to <code>fetchData()</code> decides what to do with
    // the returned data.
    // <P>
    // For example, given a ListGrid "myGrid" and a DataSource "employees", the following code
    // would populate "myGrid" with data fetched from the DataSource:
    // <pre>
    //    <smartclient>isc.DataSource.get("employees").fetchData(null, "myGrid.setData(data)");</smartclient>
    //    <smartgwt>DataSource.get("employees").fetchData(null, new DSCallback() {
    //        public void execute(DSResponse response, Object rawData, DSRequest request) {
    //            myGrid.setData(response.getData());
    //        }
    //    });</smartgwt>
    // </pre>
    // Unlike calling <code>myGrid.fetchData()</code>, which creates a +link{ResultSet}, the
    // data provided to the grid is "disconnected" data, unmanaged by SmartClient's databinding
    // facilities and safe to directly modify.  This is useful when, for example, a ListGrid is
    // being used as a more sophisticated version of HTML's multi-select component.
    // <P>
    // Disconnected datasets may be used to populate various visual components.  For example,
    // while an individual FormItem can be configured to fetch
    // +link{formItem.valueMap,valueMap} options from a DataSource via the
    // +link{formItem.optionDataSource,optionDataSource} property, the following code shows
    // storing a dataset to derive valueMaps from later:
    // <pre>
    //    <smartclient>
    //    isc.DataSource.get("countries").fetchData(null, "window.countries = data");
    //
    //    ... later, a form is created dynamically ...
    //
    //    function showForm() {
    //       isc.DynamicForm.create({
    //           items : [
    //              { name:"country", title:"Pick Country",
    //                valueMap: window.countries.getValueMap("countryId", "countryName")
    //              },
    //       ...
    //    </smartclient>
    //    <smartgwt>
    //      // Assume GlobalStore.allCountries is a public static variable of type RecordList
    //      DataSource.get("countries").fetchData(null, new DSCallback(){
    //         public void execute(DSResponse response, Object rawData, DSRequest request) {
    //            GlobalStore.allCountries = response.getDataAsRecordList();
    //         }
    //      });
    //      ... later, while a DynamicForm is being created ...
    //      SelectItem select = new SelectItem("country", "Pick Country");
    //      Map valueMap = GlobalStore.countries.getValueMap("countryId", "countryName");
    //      myItem.setValueMap(new LinkedHashMap(valueMap));
    //    </smartgwt>
    // </pre>
    // <P>
    // You can also create a ResultSet from the data retrieved from <code>fetchData()</code>,
    // like so:
    // <pre>
    //    <smartclient>
    //    isc.DataSource.get("countries").fetchData(null,
    //        function (dsResponse, data) {
    //            isc.ResultSet.create({
    //                dataSource:"countries",
    //                allRows:data
    //            })
    //        }
    //    )
    //    </smartclient>
    //    <smartgwt>
    //    DataSource.get("countries").fetchData(null, new DSCallback() {
    //        public void execute(DSResponse response, Object rawData, DSRequest request) {
    //            ResultSet rs = new ResultSet(DataSource.get("countries"));
    //            rs.setAllRows(response.getData());
    //        }
    //    });
    //    </smartgwt>
    // </pre>
    // <P>
    // This gives you a dataset that supports client-side filtering (via
    // +link{resultSet.setCriteria,setCriteria()}), can provide
    // +link{resultSet.getValueMap,filtered valueMaps}, will
    // +link{ResultSet.disableCacheSync,automatically reflect updates} to the DataSource made via
    // other components, and can be re-used with multiple visual components.
    // <P>
    // See also +link{DataSource.getClientOnlyDataSource()} and +link{DataSource.cacheAllData} for
    // similar capabilities for dealing with smaller datasets entirely within the browser, or working
    // with modifiable caches representing subsets of the data available from a DataSource.
    // <P>
    // See also the server-side com.isomorphic.js.JSTranslater class in the
    // +docTreeLink{javaServerReference,Java Server Reference} for other, similar approaches
    // involving dumping data into the page during initial page load.  <b>Note:</b> care should
    // be taken when using this approach.  Large datasets degrade the basic performance of some
    // browsers, so use +link{pickList.optionDataSource,optionDataSource} and similar
    // facilities to manage datasets that may become very large.
    // <P>
    // <b>Data-Driven Visual Component Creation</b>
    // <P>
    // <smartclient>
    // <code>DataSource.fetchData()</code> can also be used to create SmartClient components in
    // a data-driven way.  Many properties on SmartClient visual components are configured via
    // an Array of Objects - the same data format that <code>dataSource.fetchData()</code>
    // returns.  These include +link{listGrid.fields}, +link{tabSet.tabs},
    // +link{dynamicForm.items}, +link{facet.values} and even +link{dataSource.fields}.
    // <P>
    // For example, if you had a DataSource "myFormFields" whose fields included the
    // basic properties of +link{FormItem,FormItems} (name, title, type, etc), this example
    // code would create a form based on stored field definitions, loaded from the
    // "myFormFields" DataSource on the fly:
    // <pre>
    //    isc.DataSource.get("myFormFields").fetchData(null,
    //        "isc.DynamicForm.create({ items:data })"
    //    )
    // </pre>
    // </smartclient>
    // <smartgwt>
    // <code>DataSource.fetchData()</code> can also be used to create Smart GWT components in a
    // data-driven way.  For example, if you had a DataSource "myGridFields" whose fields included the
    // basic properties of {@link com.smartgwt.client.widgets.grid.ListGridField} (name, title, type,
    // etc), this example code would create a form based on stored field definitions, loaded from the
    // "myFormFields" DataSource on the fly:
    // <pre>
    //   DataSource.get("myFormFields").fetchData(null, new DSCallback(){
    //       public void execute(DSResponse response, Object rawData, DSRequest request) {
    //          Record[] records = response.getData();
    //          ListGridField[] fields = new ListGridField[records.length];
    //          for (Record record in records) {
    //              ListGridField field = new ListGridField();
    //              field.setName(record.getAttribute("name"));
    //              field.setTitle(record.getAttribute("title"));
    //              field.setType(ListGridFieldType.valueOf(record.getAttribute("type")));
    //          }
    //          ListGrid grid = new ListGrid();
    //          grid.setFields(fields);
    //       }
    //   });
    // </pre>
    // </smartgwt>
    // This capability to dynamically create visual components from dynamically fetched data
    // provides a foundation for creating interfaces that can be customized by end users.
    // See also the server-side API com.isomorphic.datasource.DataSource.addDynamicDSGenerator() for
    // dynamically creating DataSources supporting all server-side DataSource features, and
    // +link{dataSource.inheritsFrom} for sharing field definitions across multiple DataSources.
    //
    //    @param    [criteria]          (Criteria)      search criteria
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    fetchData : function (criteria, callback, requestProperties) {
        this.performDSOperation("fetch", criteria, callback, requestProperties);
    },

    //> @method dataSource.filterData()
    // Perform a "fetch" DataSource operation against this DataSource, sending search criteria
    // and retrieving matching records.
    // <P>
    // This is identical to +link{fetchData()} except that
    // +link{dsRequest.textMatchStyle} is set to "substring" to cause case insensitive
    // substring matching (if the server respects this setting).
    //
    //    @param    [criteria]          (Criteria)      search criteria
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    filterData : function (criteria, callback, requestProperties) {
        if (!requestProperties) requestProperties = {};

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        if (requestProperties.textMatchStyle == null) requestProperties.textMatchStyle = "substring";
        this.performDSOperation("fetch", criteria, callback, requestProperties);
    },

    //> @attr dataSource.emptyExportMessage (string : "You are attempting to export an empty dataset" : [IRW])
    // The message to display to the user if an export of client-side data is attempted, but
    // data provided for export is null or an empty List.
    // @see dataSource.exportClientData
    // @visibility internal - for now
    //<
    emptyExportMessage: "You are attempting to export an empty dataset",

    //> @method dataSource.exportClientData()
    // Exports arbitrary client-side data, with client-side formatters applied, so is suitable
    // for direct display to users.  This method can be used to export data formatted outside
    // of any kind of visual component.
    // <P>
    // If you do not specify an +link{operationBinding.operationId,operationId} in the
    // <code>requestProperties</code> you pass to this method, it behaves exactly the same as
    // the +link{classMethod:DataSource.exportClientData,static classMethod} of the same name.
    // If you do specify an <code>operationId</code>, the framework expects your DataSource to
    // configure an +link{class:operationBinding} of +link{operationBinding.operationType,operationType}
    // "clientExport" with the same <code>operationId</code>.  The framework will then send the
    // <code>exportClientData</code> request via the ordinary +link{class:DSRequest} mechanism,
    // which allows you to use normal framework features in the client data export.
    // <p>
    // For example, you could add a +link{group:dmiOverview,DMI declaration} to your
    // <code>operationBinding</code>, which would allow you to write server-side code that
    // intervenes in the export process - for instance, by calling the
    // <code>getExportObject()</code> API to do something special with the export document,
    // like saving it to a database table or sending it to an email list.
    // <p>
    // When you use the specific <code>operationId</code> version of this API, both the
    // SmartClient Server and server-side DataSources are required.
    // <P>
    // To export unformatted data, see +link{dataSource.exportData, exportData} which does
    // not include client-side formatters, but requires both the SmartClient server and the
    // presence of server-side DataSources.
    // <P>
    // Note that field +link{DataSourceField.displayFormat,displayFormat} is honored for
    // "date" and "datetime" fields when exporting direct to Excel; see the displayFormat
    // docs for details.
    //
    // @param data (Array of Record) Array of Records to export
    // @param requestProperties (DSRequest properties) Request properties for the export
    // @param callback (DSCallback) Optional callback.  Note that this is only applicable
    // if you also specify +link{DSRequest.exportToClient,exportToClient}: false in the
    // request properties
    // @visibility external
    //<
    exportClientData : function (data, requestProperties, callback) {
        if (requestProperties.operationId) {
            if (!this.operationBindings) {
                this.logWarn("exportClientData() was passed operationId '" +
                             requestProperties.operationId +
                             "', but this DataSource has no operationBindings.  Delegating to the " +
                             "static exportClientData() method");
                delete requestProperties.operationId;
            }
            var binding, undef;
            for (var i = 0; i < this.operationBindings.length; i++) {
                if (this.operationBindings[i].operationId == requestProperties.operationId &&
                    this.operationBindings[i].operationType == "clientExport")
                {
                    binding = this.operationBindings[i];
                }
            }
            if (binding === undef) {
                this.logWarn("exportClientData() was passed operationId '" +
                             requestProperties.operationId +
                             "', but this DataSource has no operationBinding of type 'clientExport' " +
                             "with that name.  Delegating to the static exportClientData() method");
                delete requestProperties.operationId;
            }
        }
        return isc.DataSource.exportClientData(data,requestProperties, callback, this);
    },

    //> @method dataSource.exportData()
    // Perform a "fetch" DataSource operation against this DataSource, sending search criteria,
    // retrieving matching records and exporting the results.  See
    // +link{operationBinding.exportResults} or +link{dsRequest.exportResults} and for more
    // information.
    //
    //    @param    [criteria]          (Criteria)      search criteria
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @param [callback] (DSCallback)   callback to invoke on completion.  Note that this parameter
    //                                  only applies where +link{dsRequest.exportToClient} is
    //                                  explicitly set to false, because  file downloads do not
    //                                  provide ordinary SmartClient callbacks
    // @group operations
    // @visibility external
    //<
    exportData : function (criteria, requestProperties, callback, component) {
        var operationType = "fetch";

        if (!requestProperties) requestProperties = {};

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        if (this.canExport == false) {
            // exporting is disabled at the DS level - warn and return
            isc.logWarn("Exporting is disabled for this DataSource.  Set " +
                "DataSource.canExport to true to enable it.");
            return;
        }

        if (requestProperties.exportAs && requestProperties.exportAs.toLowerCase() == "json" &&
            requestProperties.exportToClient !== false)
        {
            // json export not allowed via client-calls
            isc.logWarn("Export in JSON format with download to client is not allowed from a " +
                        "client call - either specify exportToClient: false, or set " +
                        "operationBinding.exportAs on your DataSource instead.  The server " +
                        "will reject this attempt.");
        }

        if (requestProperties.textMatchStyle == null) requestProperties.textMatchStyle = "substring";
        var exportAs = requestProperties.exportAs || "csv";

        var parameters = {};
        parameters.exportResults = true;
        parameters.exportAs = exportAs;
        parameters.exportDelimiter = requestProperties.exportDelimiter || ",";
        parameters.exportTitleSeparatorChar = requestProperties.exportTitleSeparatorChar || "";
        if (requestProperties.operationId != null) {
            var operationBinding = this.getOperationBinding(operationType, requestProperties.operationId);
            if (operationBinding != null && operationBinding.exportAs) {
                exportAs = operationBinding.exportAs;
            }
        }
        parameters.exportFilename = isc.DataSource.addExportFilenameExtension(requestProperties.exportFilename || "Results",
                                                                    exportAs);
        requestProperties.exportFilename = parameters.exportFilename;
        parameters.exportPath = requestProperties.exportPath;
        parameters.exportDisplay = requestProperties.exportDisplay || "download";
        parameters.lineBreakStyle = requestProperties.lineBreakStyle || "default";
        parameters.exportFields = this.getExportableDSFields(
            requestProperties.exportFields  || this.getVisibleDSFields(), component
        );
        parameters.exportHeader = requestProperties.exportHeader;
        parameters.exportHeaderless = requestProperties.exportHeaderless;
        parameters.exportFooter = requestProperties.exportFooter;
        parameters.exportFieldTitles = requestProperties.exportFieldTitles;
        parameters.exportDatesAsFormattedString = requestProperties.exportDatesAsFormattedString;
        parameters.exportRawValues = requestProperties.exportRawValues;
        parameters.exportCurrencySymbol = isc.NumberUtil.currencySymbol;

        if (!parameters.exportFieldTitles) {
            var wkFields = parameters.exportFields;
            var exportFieldTitles = {};
            for (var i = 0; i < wkFields.length; i++) {
                var field = wkFields[i];
                var fieldName;
                if (isc.isA.String(field)) {
                    fieldName = field;
                    field = this.getField(fieldName);
                }
                if (field) {
                    if (field.hidden) continue;
                   exportFieldTitles[field.name] = field.exportTitle || field.title;
                } else {
                   exportFieldTitles[fieldName] = fieldName;
                }
            }
            parameters.exportFieldTitles = exportFieldTitles;
        }

        parameters.exportHeaderSpans = requestProperties.exportHeaderSpans;
        parameters.exportOtherFields = requestProperties.exportOtherFields;

        if (requestProperties.exportHeaderHeight != null) {
            parameters.exportHeaderHeight = requestProperties.exportHeaderHeight;
        }

        if (requestProperties.exportFieldPixelWidths != null) {
            parameters.exportFieldPixelWidths = requestProperties.exportFieldPixelWidths;
            parameters.exportWidthScale = requestProperties.exportWidthScale;
        }

        parameters.exportWrapHeaderTitles = requestProperties.exportWrapHeaderTitles;

        if (requestProperties.exportAlignments != null) {
            parameters.exportAlignments = requestProperties.exportAlignments;
        }

        requestProperties.downloadResult = (requestProperties.exportToClient !== false);


        requestProperties.downloadToNewWindow = requestProperties.exportDisplay == "window"
            ? true : false;

        if (requestProperties.downloadToNewWindow) {
            if (parameters.exportFilename.endsWith(".xml") && parameters.exportAs != "xml") {
                parameters.exportFilename = parameters.exportFilename + ".txt";
            }
            requestProperties.download_filename = requestProperties.exportFilename;
            criteria = criteria || {};
            criteria.download_filename = requestProperties.download_filename;
        }
        requestProperties.showPrompt = false;
        requestProperties.parameters = parameters;

        this.performDSOperation(operationType, criteria, callback, requestProperties);
    },

    getVisibleDSFields : function () {
        // pass up only visible fields
        var vFields = [];
        var fields = this.fields;
        // Internally, the fields collection is held as an index, not an array, so convert
        if (!isc.isAn.Array(fields)) {
            fields = [];
            for (var key in this.fields) {
                fields.add(this.fields[key]);
            }
        }
        for (var i = 0; i < fields.length; i++) {
            var field = fields.get(i);
            if (!field.hidden) vFields.add(field.name);
        }
        return vFields;
    },

    getExportableDSFields : function (fields, component) {
        // pass up only those fields that can be exported
        var vFields = [];
        if (this.canExport) {
            for (var i = 0; i < fields.length; i++) {
                var field = this.getField(fields[i]);
                if (!field && component && component.getField) {
                    field = component.getField(fields[i]);
                    if (field && field.includeFrom && field.canExport != false) {
                        vFields.add(fields[i]);
                    }
                    continue;
                }
                if (field && field.canExport != false)
                    vFields.add(field.name);
            }
        }
        return vFields;
    },

    // undoc'd ability to disable exports at the DS level
    canExport: true,

    //> @method Callbacks.Function
    // Generic callback interface.
    //
    // @visibility external
    //<

    //> @method Callbacks.ClientOnlyDataSourceCallback
    // Generic callback interface.
    //
    // @param clientOnly (DataSource) Client only Data Source.
    // @visibility external
    //<

    //> @method dataSource.getClientOnlyDataSource()
    // Produces a clientOnly "copy" of a particular subset of data from a normal
    // DataSource, via calling fetchData() to fetch matching rows, and constructing
    // a clientOnly DataSource that +link{inheritsFrom} the original DataSource.
    // <P>
    // This clientOnly "copy" can be useful in situations where you want to allow
    // a series of local changes without immediately committing to the server.
    // See also +link{ListGrid.autoSaveEdits} for more fine-grained tracking of
    // edits (eg, special styling for uncommitted changes).
    // <P>
    // The new DataSource is returned via the "callback" argument.  If +link{cacheAllData} is
    // enabled and +link{hasAllData()} returns true, the new DataSource is synchronously
    // returned as the result of the method.  In this case, if a callback was passed, it also
    // is executed synchronously.
    //
    // @param criteria (Criteria) The criteria for the clientOnly DS
    // @param callback (ClientOnlyDataSourceCallback) The callback to fire passing the clientOnly DS
    // @param [requestProperties] (DSRequest Properties) optional properties to pass through to the DSRequest
    // @param [dataSourceProperties] (DataSource Properties) optional properties to pass
    //                                                       through to the clientOnly DS
    //
    // @visibility external
    //<
    getClientOnlyDataSource : function (criteria, callback, requestProperties, dataSourceProperties) {
        var _criteria = criteria,
            _callback = callback,
            parentDS = this;

        if (this.cacheAllData && this.hasAllData()) {
            var ds = isc.DataSource.create({
                inheritsFrom: parentDS,
                clientOnly: true,
                useParentFieldOrder: true,
                testData: this.cacheResultSet.getAllRows()
                }, dataSourceProperties)
            ;
            parentDS.fireCallback(_callback, "dataSource", [ds]);
            return ds;
        } else {
            this.fetchData(_criteria,
                function (dsResponse, data) {
                    var ds = isc.DataSource.create({
                        inheritsFrom: parentDS,
                        clientOnly: true,
                        useParentFieldOrder: true,
                        testData: data
                        }, dataSourceProperties)
                    ;
                    parentDS.fireCallback(_callback, "dataSource", [ds]);
                },
                isc.addProperties({}, requestProperties)
            );
        }
    },

    //> @method dataSource.addData()
    // Perform an "add" DataSource operation against this DataSource, to create a new DataSource
    // record.
    // <P>
    // <b>NOTE:</b> do not use this method to populate a +link{dataSource.clientOnly,clientOnly} DataSource.  Set
    // +link{cacheData} instead.
    //
    //    @param    newRecord           (Record)      new record
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    addData : function (newRecord, callback, requestProperties) {
        if (isc.Offline && isc.Offline.isOffline() && this.contactsServer()) {
            isc.logWarn("Data cannot be saved because you are not online");
            return;
        }
        this.performDSOperation("add", newRecord, callback, requestProperties);
    },

    //> @method dataSource.updateData()
    // Perform an "update" DataSource operation against this DataSource, to update values in an
    // existing DataSource record.
    //
    //    @param    updatedRecord       (Record Properties)      updated record
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    updateData : function (updatedRecord, callback, requestProperties) {
        if (isc.Offline && isc.Offline.isOffline() && this.contactsServer()) {
            isc.logWarn("Data cannot be saved because you are not online");
            return;
        }
        this.performDSOperation("update", updatedRecord, callback, requestProperties);
    },

    //> @method dataSource.removeData()
    // Perform a "remove" DataSource operation against this DataSource, to delete an existing
    // DataSource record.
    //
    //    @param    data                (Record | PrimaryKeys) primary key values of record to delete, (or complete record)
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    removeData : function (recordKeys, callback, requestProperties) {
        if (isc.Offline && isc.Offline.isOffline() && this.contactsServer()) {
            isc.logWarn("Data cannot be saved because you are not online");
            return;
        }
        // if passed a full record, trim it down to primaryKeys.
        // This will avoid problems where additional properties such as _selected etc
        // are present on the record

        var keyFields = this.getPrimaryKeyFields(),
            recordKeys = isc.applyMask(recordKeys, keyFields);
        this.performDSOperation("remove", recordKeys, callback, requestProperties);
    },

    contactsServer : function () {
        return !this.clientOnly && this.dataProtocol != "clientCustom"
    },

    //> @method dataSource.validateData()
    // Contacts the server to run server-side validation on a DSRequest and either returns
    // +link{dsResponse.errors} validation errors or a +link{dsResponse.status} code of 0.
    // <p>
    // A "validate" dsRequest is effectively always +link{RPCRequest.willHandleError}:true.
    // It is a normal condition for a "validate" DSResponse to have validation errors and
    // the response will never go to system-wide handling for unexpected errors
    // (+link{RPCManager.handleError}).
    //
    // @param values (Record) record values to validate
    // @param [callback] (DSCallback) callback to invoke on completion
    // @param [requestProperties] (DSRequest Properties) additional properties to set on
    //                                                   the DSRequest that will be issued
    //
    // @group operations
    // @visibility external
    //<
    validateData : function (values, callback, requestProperties) {
        if (!requestProperties) requestProperties = {};

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        // Force willHandleError: true on request and default value for validationMode
        requestProperties = isc.addProperties(requestProperties, {willHandleError: true});
        if (requestProperties.validationMode == null) requestProperties.validationMode = "full";

        return this.performDSOperation("validate", values, callback, requestProperties);
    },

    //> @method dataSource.performCustomOperation()
    // Invoke an operation declared with +link{operationBinding.operationType} "custom".
    // <P>
    // <b>This is a rarely used API.</b>  If the operation you are performing can be thought of
    // as one of the standard "CRUD" +link{type:DSOperationType,operation types}, declare it
    // with a CRUD operationType.  For example, if your operation updates a record, declare it
    // with operationType "update" and invoke it via +link{updateData()} - this will cause
    // +link{ResultSet,cache sync} to work correctly.
    // <P>
    // In particular:
    // <ul>
    // <li> do not use this API just because you need to add additional server-side logic to a
    // CRUD operation (+link{DMI} allows this)
    // <li> do not use this API to implement variants of core CRUD operations
    // (+link{dsRequest.operationId} is the correct way to do this)
    // <li> do not use this API just because an operation affects more than one record.  Most
    // kinds of multi-record operations should use +link{RPCManager.startQueue(),queuing}.
    // However, a custom operation <em>is</em> appropriate for genuine "batch" updates, as
    // opposed to just a number of ordinary updates by primaryKey - see
    // +link{operationBinding.allowMultiUpdate}
    // <li> do not use this API just because you are calling a stored procedure in SQL - if the
    // stored procedure performs some kind of CRUD operation on the records of this DataSource,
    // use a standard CRUD operationType
    // </ul>
    // <P>
    // Instead, the specific purpose of this API is to bypass all checks and side effects that
    // normally occur for CRUD operations, for example, that a "fetch" requires valid Criteria
    // or that an "update" or "remove" operation contains a valid primary key, or that an "add"
    // operation returns the newly added record.  <code>performCustomOperation</code> allows
    // you to pass an arbitrary Record to the server, act on it with custom code, and return
    // arbitray results or even no results.
    // <P>
    // The "data" parameter becomes +link{dsRequest.data,dsRequest.data}.  With the SmartClient
    // Server Framework, the data is accessible server-side via DSRequest.getValues() and in
    // +link{group:velocitySupport,Velocity templates} (such as &lt;customSQL&gt;) as $values.
    // <P>
    // Note that with SQLDataSource, <code>performCustomOperation</code> must be used if you
    // plan to have a &lt;customSQL&gt; tag in your operationBinding that will execute SQL
    // operations other than SELECT, UPDATE, INSERT, DELETE (such as creating a new table).
    // By declaring +link{operationBinding.operationType} "custom" in your .ds.xml file, all
    // checks related to normal CRUD operations will be skipped and your &lt;customSQL&gt;
    // can do arbitrary things.
    //
    //  @param  operationId         (String)                 the operation ID
    //    @param    [data]              (Record Properties)      data to pass to the server.
    //    @param    [callback]          (DSCallback)  callback to invoke on completion
    //  @param  [requestProperties] (DSRequest Properties)   additional properties to set on
    //                                                       the DSRequest that will be issued
    // @group operations
    // @visibility external
    //<
    performCustomOperation : function (operationId, data, callback, requestProperties) {
        if (!requestProperties) requestProperties = {};

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        isc.addProperties(requestProperties, {operationId: operationId});
        this.performDSOperation("custom", data, callback, requestProperties);
    },

    performClientExportOperation : function (operationId, data, callback, requestProperties) {
        if (!requestProperties) requestProperties = {};

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        isc.addProperties(requestProperties, {operationId: operationId});
        this.performDSOperation("clientExport", data, callback, requestProperties);
    },

    

    _getNextRequestId : function () {
        // ID can be completely arbitrary as long as it's unique, but lets use a format like
        // "SupplyItem_request12" to simplify debugging.
        if (!this._requestIdTemplate) this._requestIdTemplate = [this.getID(), "_request"];
        this._requestIdTemplate[2] = isc.DataSource._getNextRequestId();
        return this._requestIdTemplate.join(isc.emptyString);
    },

    // In order to avoid IE9 leaks we want to use strict JSON parsing (via JSON.parse)
    // by default for iscServer dataSources

    shouldUseStrictJSON : function (request) {
        if (request && request.useStrictJSON != null) return request.useStrictJSON;
        if (this.useStrictJSON != null) return this.useStrictJSON;

        if (isc.Browser.isIE && isc.Browser.version>=9
             && isc.RPCManager.useJSONParse_IE9 &&
            this.dataFormat == "iscServer")
        {
            return true;
        }
        return null;
    },
    shouldFallbackToEval : function (request) {
        if (request == null) request = {};
        if (request.fallbackToEval != null) return request.fallbackToEval;
        if (request.useStrictJSON || this.useStrictJSON) return false;
        return (isc.Browser.isIE && isc.Browser.version>=9
                && isc.RPCManager.useJSONParse_IE9 && this.dataFormat == "iscServer");
    },

    performDSOperation : function (operationType, data, callback, requestProperties) {
        if (isc._traceMarkers) arguments.__this = this;

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        // form a dsRequest
        var dsRequest = isc.addProperties({
            operationType : operationType,
            dataSource : this.ID,
            data : data,
            callback : callback,
            requestId : this._getNextRequestId(),
            // support useStrictJSON at the DS and at the
            // request level.

            useStrictJSON : this.shouldUseStrictJSON(requestProperties),
            fallbackToEval : this.shouldFallbackToEval(requestProperties),
            textMatchStyle : this.defaultTextMatchStyle
        }, requestProperties);

        isc.DataSource.recordTimingData(dsRequest, "UI event to DSRequest creation", "start", isc.EH._setThreadTimeStamp);
        isc.DataSource.recordTimingData(dsRequest, "UI event to DSRequest creation", "end");
        isc.DataSource.recordTimingData(dsRequest, "performDSOperation", "start");
        dsRequest.lastClientEventThreadCode = isc.EH._thread;

        var ev = isc.EH.getLastEvent();
        if (dsRequest.sortBy != null) {
            if (!isc.isAn.Array(dsRequest.sortBy)) dsRequest.sortBy = [dsRequest.sortBy];

            if (isc.isAn.Object(dsRequest.sortBy[0])) {
                dsRequest.sortBy = isc.DS.getSortBy(dsRequest.sortBy);
            }

            for (var i = 0; i < dsRequest.sortBy.length; i++) {
                var sortByProp = dsRequest.sortBy[i];
                if (!isc.isA.String(sortByProp)) continue;
                // get the sortField, which might be prefixed with -
                var field = this.getField(sortByProp.charAt(0) == "-" ?
                                sortByProp.substring(1) : sortByProp);
                // don't request the sort if the field is canSortClientOnly
                if (field && field.canSortClientOnly) dsRequest.sortBy[i] = null;
            }
            dsRequest.sortBy.removeEmpty();
            if (dsRequest.sortBy.length == 0) delete dsRequest.sortBy;
        }
        if (this.sendParentNode == false) dsRequest.parentNode = null;

        return this.sendDSRequest(dsRequest);
    },

    translatePatternOperatorInAdvancedCriteria : function(advancedCriteria) {
        if(!this.isAdvancedCriteria(advancedCriteria)) {
            return advancedCriteria;
        }

        var index, criteria;
        if (advancedCriteria.criteria) {
            if(isc.isA.Array(advancedCriteria.criteria)) {
                for(index in advancedCriteria.criteria) {
                    criteria = advancedCriteria.criteria[index];
                    advancedCriteria.criteria[index] = this.translatePatternOperatorInAdvancedCriteria(criteria);
                }
            } else {
                advancedCriteria.criteria = this.translatePatternOperatorInAdvancedCriteria(advancedCriteria.criteria);
            }
        } else {
            criteria = advancedCriteria;
            if (criteria.operator === "matchesPattern" || criteria.operator === "iMatchesPattern" ||
                criteria.operator === "containsPattern" || criteria.operator === "iContainsPattern")
            {
                var insensitive = (criteria.operator.indexOf('i') === 0);

                var splitCharacter = (isc.isA.Array(this.patternMultiWildcard) ? this.patternMultiWildcard[0] : this.patternMultiWildcard.substring(0, 1));
                var fieldName = criteria.fieldName;
                var fieldValue = criteria.value;

                if(isc.isA.Array(this.patternMultiWildcard)) {
                    for(var index = 1; index < this.patternMultiWildcard.length; index++) {
                        fieldValue = fieldValue.replaceAll(this.patternMultiWildcard[index], splitCharacter);
                    }
                }

                var parts = fieldValue.split(splitCharacter);
                var partCrit;
                var newCriteria;

                // If we only have one part, that means it's just a simple value and we can
                // create an equals criteria.
                if(parts.length === 1) {
                    if (criteria.operator === "matchesPattern" || criteria.operator === "iMatchesPattern") {
                        newCriteria = {
                            fieldName: fieldName,
                            value: parts[0],
                            operator: insensitive ? "iEquals" : "equals"
                        };
                    } else {
                        newCriteria = {
                            fieldName: fieldName,
                            value: parts[0],
                            operator: insensitive ? "iContains" : "contains"
                        };
                    }
                } else if (parts.length > 1) { // More than one part indicates we have wildcards
                    newCriteria ={
                        _constructor: "AdvancedCriteria",
                        operator: "and",
                        criteria: []
                    };

                    for (var partIndex=0; partIndex<parts.length; partIndex++) {
                        var part = parts[partIndex];

                        if (!part || part.length == 0) {
                            continue;
                        }

                        partCrit = {
                            fieldName: fieldName,
                            value: part
                        };

                        var hasPrefix = partIndex > 0,
                            hasSuffix = parts.length - 1 > partIndex;

                        if (hasPrefix && hasSuffix || (criteria.operator === "containsPattern" || criteria.operator === "iContainsPattern")) {
                            // this is a contains criteria
                            partCrit.operator = insensitive ? "iContains" : "contains";
                        } else if (hasPrefix) {
                            // this is an endsWith criteria
                            partCrit.operator = insensitive ? "iEndsWith" : "endsWith";
                        } else if (hasSuffix) {
                            // this is a startsWith criteria
                            partCrit.operator = insensitive ? "iStartsWith" : "startsWith";
                        }

                        newCriteria.criteria.add(partCrit);
                    }
                }

                advancedCriteria = newCriteria;
            }
        }

        return advancedCriteria;
    },


    sendDSRequest : function (dsRequest) {
        // provide default requestProperties for the operationBinding and the DataSource as a
        // whole
        isc.addDefaults(dsRequest,
                        this.getOperationBinding(dsRequest.operationType).requestProperties);
        isc.addDefaults(dsRequest, this.requestProperties);

        // check for iscServer missing, and switch to clientOnly mode if necessary
        var dataFormat = this.getDataFormat(dsRequest);
        var protocol = this.getDataProtocol(dsRequest);
        if (dataFormat == "iscServer" && !(this.clientOnly || protocol == "clientCustom") &&
            !isc.hasOptionalModule("SCServer"))
        {
            if (this.dataURL == null && this.testFileName == null) {
                this.logError("DataSource: "+this.ID+
                          ": attempt to use DataSource of type iscServer without SmartClient Server option."+
                          " Please either set clientOnly: true for one-time fetch against"+
                          " dataURL/testFileName or upgrade to SmartClient Pro, Power or Enterprise");
                return;
            }
            // have dataURL or testFileName, default to clientOnly mode automatically
            this.logInfo("Switching to clientOnly - no SmartClient Server installed.");
            this.clientOnly = true;
        }

        if (dsRequest.bypassCache == null) {
            dsRequest.bypassCache = this.shouldBypassCache(dsRequest);
        }

        // default showPrompt:true according to DS setting
        if (dsRequest.showPrompt == null) {
            dsRequest.showPrompt = dsRequest.downloadResult ? false : this.showPrompt;
        }

        // if cacheAllData:false and autoCacheAllData:true and this request will fetch all data,
        // switch cacheAllData on and proceed
        // Note 'downloadResult' requests bypass the cache-data so don't allow these to toggle
        // this.cacheAllData
        if (!this.cacheAllData && this.autoCacheAllData &&
                dsRequest.downloadResult != true &&
                dsRequest.operationType == "fetch" &&
                dsRequest.startRow == null && dsRequest.endRow == null &&
                (dsRequest.data == null || isc.isAn.emptyObject(dsRequest.data)))
        {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("sendDSRequest: switching on cacheAllData", "cacheAllData");
            }
            this.cacheAllData = true;
            this._autoCacheAllData_timestamp = isc.timestamp();
        }


        // If this is a clientOnly / cacheAllData dataSource and we don't yet have
        // a cache to fetch against, bail.
        // This method will kick off the one time fetch for the data as necessary and add
        // the method to the deferredRequests queue to be processed when that fetch returns.
        if (dsRequest.operationType == "fetch" && this.fetchingClientOnlyData(dsRequest)) return;

        if (this.logIsDebugEnabled()) {
            this.logDebug("Outbound DSRequest: " + this.echo(dsRequest));
        }
        // store off the dataSource layer callback as we will be passing a different callback
        // to the RPC layer
        dsRequest._dsCallback = dsRequest.callback;



        // Winnow the values object as follows:
        // - If dataSource.sparseUpdates is set, we remove values:
        //   - That are not present in this component's fields list
        //   - That are present but have an identical value in oldValues
        //   - But we keep all primaryKey fields
        // - If DataSource.noNullUpdates is set, we:
        //   - Remove properties from the values object where the value is null, even if it
        //     isn't null in oldValues, if this is an add operation.
        //   - Replace the null value with one of the following, if this is an update operation:
        //     - The field's nullReplacementValue, if it has one
        //     - Otherwise the DataSource's nullStringValue, nullIntegerValue, nullFloatValue
        //       or nullDateValue, depending on the field's underlying base SimpleType
        var opType = dsRequest.operationType;
        if ((opType == "update" || opType == "add") &&
            (this.sparseUpdates || this.noNullUpdates))
        {
            isc.DataSource.recordTimingData(dsRequest, "Sparse updates processing", "start");
            // Work on a copy of the data - otherwise, we could trim away values that users
            // are actually looking at, and a redraw between now and cache sync would make
            // them disappear...
            dsRequest.data = this._cloneValues(dsRequest.data, this);
            this._applySparseAndNoNullUpdates(dsRequest.data, dsRequest.oldValues, opType, this);
            isc.DataSource.recordTimingData(dsRequest, "Sparse updates processing", "end");
        }

        if (dataFormat == "iscServer") {
            // since dataFormat can be set dynamically in transformRequest, we need
            // to store the request as if it is a client-custom request so we can
            // process it using the client-custom flow.
            this._storeCustomRequest(dsRequest);


            dsRequest.unconvertedDSRequest = isc.shallowClone(dsRequest);

            // This is a "reviver" function ultimately passed to JSON.parse
            // if useStrictJSON is true for this method.
            // Our built in version for iscServer format requests will
            // parse dates from special formatted strings.
            // This will have no effect if useStrictJSON is not set on the request.
            dsRequest.jsonReviver = isc.DataSource.iscServer_strictJSONReviver;

            if (this.autoConvertRelativeDates == true) {
                // convert any relative dates in criteria into absolute dates so the server
                // doesn't need to know how to handle relative dates
                if (this.logIsInfoEnabled("relativeDates")) {
                    this.logInfo("Calling convertRelativeDates from sendDSRequest "+
                        "- data is\n\n"+isc.echoFull(data));
                }

                var data = this.convertRelativeDates(dsRequest.data);

                if (this.logIsInfoEnabled("relativeDates")) {
                    this.logInfo("Called convertRelativeDates from sendDSRequest "+
                        "- data is\n\n"+isc.echoFull(data));
                }

                dsRequest.data = data;
            }

            // If we're supposed to translate matchesPattern operator to other operators and the
            // request contains an advanced criteria, lets translate.
            if(this.translatePatternOperators === true && this.isAdvancedCriteria(dsRequest.data)) {
                dsRequest.data = this.translatePatternOperatorInAdvancedCriteria(dsRequest.data);
            }

            var transformedData = this.transformRequest(dsRequest);

            // correct the common error of returning the dsRequest itself incorrectly, which is
            // never right since the dsRequest contains various widgets and other data
            // inappropriate to send to the server.
            if (transformedData !== dsRequest) {
                data = dsRequest.data = transformedData;
            }

            var updatedProtocol = this.getDataProtocol(dsRequest);

            // If this is a clientCustom operation we're done
            // (protocol can be set dynamically in the transformRequest method
            // - hence the 'updatedProtocol' check)
            // This implies the transformRequest implementation will have
            // kicked off a request and we'll be notified via an explicit call to
            // processResponse() when new data is available.

            if (updatedProtocol == "clientCustom") {
                // _isServerRequest is installed when cacheAllData is active
                if (!this._isServerRequest) return;
                // only bail here for server requests - otherwise, use the cache as usual
                if (this._isServerRequest(dsRequest)) {
                    this.logInfo("sendDSRequest: clientCustom server request - returning",
                        "cacheAllData"
                    );
                    return;
                } else {
                    this.logInfo("sendDSRequest: clientCustom request using client cache",
                        "cacheAllData"
                    );

                    delete this._clientCustomRequests[dsRequest.requestId];
                }
            } else {
                // We now know this is not a client-custom operation - clear out the
                // request from the client-custom store so we don't leak requests.
                delete this._clientCustomRequests[dsRequest.requestId];
            }

            // detect whether this is a clientOnly request
            if ( dsRequest.shouldUseCache === false ||
                (dsRequest.shouldUseCache !== true) &&
                !this.clientOnly &&
                (!this.cacheAllData || dsRequest.downloadResult || dsRequest.cachingAllData ||
                 (dsRequest.operationType != null && dsRequest.operationType != "fetch") ||
                 (dsRequest.operationType == "fetch" && this.cacheAllOperationId &&
                     this.cacheAcrossOperationIds == false &&
                     dsRequest.operationId != this.cacheAllOperationId)))
            {
                // not client-only: this is a request to the SC server

                if (this.logIsInfoEnabled("cacheAllData") && dsRequest.cachingAllData) {
                    // special request from a cacheAllData DataSource to populate cache
                    this.logInfo("sendDSRequest: processing cacheAllData request", "cacheAllData");
                }

                this.addDefaultCriteria(dsRequest, this.getOperationBinding(dsRequest));
                this.applySendExtraFields(dsRequest);

                var operationBinding = this.getOperationBinding(dsRequest);
                if (operationBinding == null) operationBinding = {};
                var defaultParams = operationBinding.defaultParams || this.defaultParams;
                if (defaultParams) {
                    dsRequest.params = isc.addProperties({}, defaultParams, dsRequest.params);
                }
                data = dsRequest.data;

                return this.performSCServerOperation(dsRequest, data);
            }
        }


        var inputs = this.getServiceInputs(dsRequest);

        // client only requests: will be handled entirely in-browser, by returning results from
        // an in-browser cache.

        var internalCacheRequest = dsRequest.shouldUseCache === true ||
                                    (this.cacheAllData &&
                                    dsRequest.operationType == "fetch" &&
                                    !dsRequest.cachingAllData &&
                                    !dsRequest.downloadResult &&
                                    !(this.cacheAllOperationId &&
                                    this.cacheAcrossOperationIds == false &&
                                    dsRequest.operationId != this.cacheAllOperationId));

        // clientCustom dataProtocol - in this case we assume transformRequest has been implemented
        // to call a server via custom code or get data in some non-network manner (eg from an
        // Applet).  transformRequest was called by getServerInputs() above, so we're done -
        // we're now waiting for the developer's code to call processResponse with a
        // dsResponse.

        if (inputs.dataProtocol == "clientCustom" && !internalCacheRequest) return;


        // form an rpcRequest for this dsRequest
        var rpcRequest = isc.addProperties({}, dsRequest, inputs);
        // hold onto the dsRequest that initiated this call, and the indirect callback
        rpcRequest._dsRequest = dsRequest;

        // HACK: if we don't do this, when this request is passed to rpcRequest.send(),
        // dsRequest.data gets passed as a requestBody to the proxy servlet
        if (inputs.data == null) rpcRequest.data = null;

        if (this.clientOnly || internalCacheRequest) {
            rpcRequest.clientOnly = true;
            rpcRequest.callback = {target:this, methodName:"_handleClientOnlyReply" };

            isc.RPC.sendRequest(rpcRequest);
            return;
        }

        var opBinding = this.getOperationBinding(dsRequest);
        rpcRequest.transport = opBinding.dataTransport || this.dataTransport;
        if (rpcRequest.transport == "scriptInclude") {
            // cross-site JSON: we will receive live JS objects directly
            rpcRequest.callback = { target:this, methodName:"_handleJSONReply" };
            // transformResponse or previous application via requestProperties in
            // performDSOperation takes precedence
            if (!rpcRequest.callbackParam) {
                rpcRequest.callbackParam = opBinding.callbackParam || this.callbackParam;
            }
            isc.rpc.sendRequest(rpcRequest);
            return;
        }

        var dataFormat = this.getDataFormat(dsRequest);
        // XML pathway: result will be parsed as XML, have recordXPath and schema applied
        if (dataFormat == "xml") {
            var spoofedResponse = rpcRequest.spoofedResponse;
            if (!spoofedResponse) {
                rpcRequest.callback = { target:this, method: this._handleXMLReply };
                isc.xml.getXMLResponse(rpcRequest);
            } else {
                var ds = this;
                isc.Timer.setTimeout(function () {
                    ds._handleXMLReply(isc.xml.parseXML(spoofedResponse),
                                       spoofedResponse,
                                       { status:0, httpResponseCode:200,
                                         data:spoofedResponse },
                                       rpcRequest);
                });
            }

        // JSON pathway: result will be eval()d as JSON, have recordXPath and schema applied
        } else if (dataFormat == "json") {
            rpcRequest.callback = { target:this, method: this._handleJSONTextReply };
            isc.rpc.sendProxied(rpcRequest);

        // CSV pathway: will be converted to JSON and then processed as usual
        } else if (dataFormat == "csv") {
            rpcRequest.callback = { target:this, method: this._handleCSVTextReply };
            isc.rpc.sendProxied(rpcRequest);

        // custom pathway: you receive the raw String result and must fill out a DSResponse
        } else {
            rpcRequest.serverOutputAsString = true;
            rpcRequest.callback = { target:this, method:this._handleCustomReply };
            isc.rpc.sendProxied(rpcRequest);
        }
    },

    _applySparseAndNoNullUpdates : function (values, oldValues, operationType, paramDS) {

        var ds = paramDS || this;

        if (!ds.noNullUpdates) {
            if (!ds.sparseUpdates) return;
            if (oldValues == null) return;
        }

        var propertiesToSkip = {
            __ref: true,
            __module: true
        };

        for (var key in values) {

            // Don't touch functions, classes, instances or properties explicitly called out
            // in the proeprtiesToSkip object above
            if (isc.isA.Function(values[key])) continue;
            if (propertiesToSkip[key] == true) continue;
            if (isc.isAn.Instance(values[key]) || isc.isA.Class(values[key])) continue;

            var value = values[key];
            if (ds.noNullUpdates && value === null) {
                if (operationType == "add" && ds.omitNullDefaultsOnAdd == true) {
                    delete values[key];
                } else {
                    var field = ds.getField(key),
                        undef;
                    if (field && field.nullReplacementValue !== undef) {
                        values[key] = field.nullReplacementValue;
                    } else {
                        var type = isc.SimpleType.getBaseType(field.type, ds);
                        if (type == "integer") {
                            values[key] = ds.nullIntegerValue;
                        } else if (type == "float") {
                            values[key] = ds.nullFloatValue;
                        } else if (type == "date" || type == "time") {
                            values[key] = ds.nullDateValue;
                        } else if (type == "boolean") {
                            values[key] = ds.nullBooleanValue;
                        } else {   // Anything else, use the string default
                            values[key] = ds.nullStringValue;
                        }

                    }
                }
            // Sparse updates only apply to "update" operations
            } else if (ds.sparseUpdates && operationType == "update") {
                if (oldValues == null) continue;
                var oldValue = oldValues[key];
                if (oldValue == null && !(value == null)) continue;
                if (ds != null) {
                    var field = ds.getField(key);
                    if (field && field.primaryKey) continue;
                }
                if (isc.isA.Date(value) && Date.compareDates(value, oldValue) == 0) {
                    delete values[key];
                } else if (isc.isAn.Array(value)) {
                    for (var i = 0; i < value.length; i++) {
                        this._applySparseAndNoNullUpdates(value[i], oldValue[i], operationType,
                                        field == null ? null : isc.DataSource.get(field.type));
                        var keyCount = 0;
                        for (var k2 in value[i]) keyCount++;
                        if (keyCount == 0) delete value[i];
                    }
                    // If this process leaves an array full of nulls, don't bother serializing it
                    var goodEntry = false;
                    for (var i = 0; i < value.length; i++) {
                        if (value[i] != null) {
                            goodEntry = true;
                            break;
                        }
                    }
                    if (!goodEntry) delete values[key];
                } else if (isc.isAn.Object(value)) {
                    this._applySparseAndNoNullUpdates(value, oldValue, operationType,
                                        field == null ? null : isc.DataSource.get(field.type));
                    // If this process leaves an object with no properties, get rid of it
                    var keyCount = 0;
                    for (var k2 in value) keyCount++;
                    if (keyCount == 0) delete values[key];
                } else if (value == oldValue) {
                    delete values[key];
                }
            }
        }
    },


    _cloneValues : function (values, dataSource, dupList) {

        if (values == null) return;

        var dsDeepClone = dataSource ? dataSource.deepCloneOnEdit : this.deepCloneOnEdit,
            deepClone = dsDeepClone == null ? isc.DataSource.deepCloneOnEdit
                                              : dsDeepClone;

        if (isc.isAn.Array(values)) {
            var storedValues = [];
            for (var i = 0; i < values.length; i++) {
                var value = values[i];
                if (isc.isA.Function(value)) continue;
                if (isc.isAn.Instance(values[prop]) || isc.isA.Class(values[prop])) continue;

                if (value == null || isc.isA.String(value) || isc.isA.Boolean(value) ||
                    isc.isA.Number(value))
                {
                    storedValues[storedValues.length] = value;

                } else if (isc.isA.Date(value)) {
                    storedValues[storedValues.length] = new Date(value.getTime());

                } else if (isc.isAn.Object(value)) {
                    storedValues[storedValues.length] = this._cloneValues(value,
                                                                dataSource, dupList);
                }
            }
            return storedValues;
        }

        var storedValues = {};


        if (values._isc_tree != null) {
            values = isc.JSONEncoder._serialize_cleanNode(values)
        }

        var propertiesToSkip = {
            __ref: true,
            __module: true
        };

        if (isc.DataSource.cloneValuesSafely) {
            if (!dupList) dupList = [];
            if (dupList.contains(values)) {
                storedValues = values;
                return;
            }
            dupList.add(values);
        }

        for (var prop in values) {
            if (isc.isA.Function(values[prop])) continue;

            if (propertiesToSkip[prop] == true) continue;

            if (isc.isAn.Instance(values[prop]) || isc.isA.Class(values[prop])) continue;

            // Special case for dates - duplicate them rather than copying the object
            // across
            var propValue = values[prop];
            if (isc.isA.Date(propValue)) {
                storedValues[prop] = propValue.duplicate();

            } else if (isc.isAn.Object(propValue)) {   // This case works for arrays as well
                var field = dataSource ? dataSource.getField(prop) : null;
                if (!field) {
                    storedValues[prop] = values[prop];
                } else {
                    // Only deep-clone objects corresponding to fields that have deepCloneOnEdit
                    // set (or inherited from the DataSource or from the static setting on
                    // isc.DataSource)
                    if (field.deepCloneOnEdit == true ||
                           (field.deepCloneOnEdit == null && deepClone))
                    {
                        // If the global DataSource flag "cloneValuesSafely" is set, keep track of
                        // objects we've seen before and shallow clone any duplicates
                        if (isc.DataSource.cloneValuesSafely) {
                            if (dupList.contains(propValue)) {
                                storedValues[prop] = values[prop];
                                continue;
                            }
                            dupList.add(propValue);
                        }
                        storedValues[prop] = this._cloneValues(propValue,
                            isc.DataSource.get(field.type));
                    } else {
                        storedValues[prop] = values[prop];
                    }
                }
            } else {
                storedValues[prop] = values[prop];
            }

        }
        return storedValues;
    },

    //> @method DataSource.useOfflineResponse()
    // Override point to allow application code to suppress use of a particular offline
    // response.  For example, application code may wish to examine the response's
    // +link{DSResponse.offlineTimestamp,offlineTimestamp} to make a decision about whether
    // the response is too stale to be useful.
    // <p>
    // This is an application override point only; there is no default implementation.
    //
    // @param dsRequest (DSRequest) The dsRequest object
    // @param dsResponse (DSResponse) The corresponding dsResponse object returned from
    //                                offline cache
    // @return (boolean) true to allow this response to be used, false to prevent it
    // @group offlineGroup
    // @visibility external
    //<




    fulfilledFromOffline : function (dsRequest) {
        var cachedRequest = dsRequest.unconvertedDSRequest ?
                            dsRequest.unconvertedDSRequest : dsRequest;
        if (this.useOfflineStorage && isc.Offline) {
            var requestString = dsRequest.dataSource + "." + dsRequest.operationType;
            if (isc.Offline.isOffline()) {
                var dsResponse = isc.Offline.getResponse(cachedRequest);
                this.logInfo("currently offline, for request: " + requestString +
                             " found cached response: " + this.echoLeaf(dsResponse),
                             "offline");
                if (this.useOfflineResponse && !this.useOfflineResponse(cachedRequest, dsResponse)) {
                    this.logInfo("User-written useOfflineResponse() method returned false; " +
                                 "not using cached response", "offline");
                    dsResponse = null;
                }
                this.processOfflineResponse(dsRequest, dsResponse);
                // whether there's a cached response or not, we're done
                return true;
            } else if (cachedRequest.useOfflineCache || cachedRequest.useOfflineCacheOnly) {
                var dsResponse = isc.Offline.getResponse(cachedRequest);
                if (dsResponse != null) {
                    // found a cached response, return it
                    this.logInfo("request: " + requestString +
                                 ", returning cached offline response", "offline");
                    if (this.useOfflineResponse && !this.useOfflineResponse(cachedRequest, dsResponse)) {
                        this.logInfo("User-written useOfflineResponse() method returned false; " +
                                     "not using cached response", "offline");
                        dsResponse = null;
                    }
                    this.processOfflineResponse(dsRequest, dsResponse);
                    return true;
                } else if (dsRequest.useOfflineCacheOnly) {
                    // no cached response, but we're only allowed to use offline data
                    this.logInfo("request: " + requestString +
                                 ": useOfflineCacheOnly: no response available", "offline");
                    this.processOfflineResponse(dsRequest);
                    return true;
                }
                this.logInfo("request: " + requestString +
                             ", no cached response, proceeding with network request", "offline");
            }
        }
        return false;
    },

    processOfflineResponse : function (dsRequest, dsResponse) {
        // NOTE you get this response if the network is not available but also if you set
        // useOfflineCacheOnly and no cached response is available
        if (!dsResponse) {
            dsResponse = {
                status: isc.RPCResponse.STATUS_OFFLINE,
                data: isc.DataSource.offlineMessage
            };
        }

        dsResponse.clientContext = dsRequest.clientContext;
        dsResponse.internalClientContext = dsRequest.internalClientContext;
        dsResponse.isCachedResponse = true;
        // this injects the cached response into the processing chain right at the point
        // where it was previously stored off
        this.fireResponseCallbacks(dsResponse, dsRequest);
    },

    performSCServerOperation : function (dsRequest, data) {
        this.logWarn("Attempt to perform iscServer request requires options SmartClient server " +
                     "support - not present in this build.\nRequest details:"+
                     this.echo(dsRequest));
        return;
    },

    //> @method dataSource.handleError() (A)
    // If you define this method on a DataSource, it will be called whenever the server returns
    // a DSResponse with a status other than +link{RPCResponse.STATUS_SUCCESS}.  You can use
    // this hook to do DataSource-specific error handling.  Unless you return
    // <code>false</code> from this method, +link{RPCManager.handleError()} will be called by
    // SmartClient right after this method completes.
    //
    // @param response (DSResponse) the DSResponse or DSResponse object returned from the server
    // @param request (DSRequest) the DSRequest or DSRequest that was sent to the server
    //
    // @return (boolean) false to suppress +link{RPCManager.handleError()}
    //
    // @group errorHandling
    // @see RPCManager.handleError()
    // @visibility external
    //<

    //> @type DSCallback
    // A +link{type:Callback} to evaluate when a DataSource request completes.
    // <P>
    // The parameters available in the DSCallback expression are:
    // <ul>
    // <li>dsResponse: a +link{class:DSResponse} instance with metadata about the returned data
    // <li>data: data returned to satisfy the DataSource request.  See the
    //     +link{group:dataSourceOperations,DataSource operations} topic for expected results
    //     for each type of DataSource operation
    // <li>dsRequest: the +link{class:DSRequest} that was sent.  You can use
    //     +link{attr:dsRequest.clientContext} to track state during the server turnaround.
    // </ul>
    // For example, if you had a DynamicForm with ID "myForm" and you wanted to retrieve a
    // record from a DataSource "myUsers", where each record has a "userId" field:
    // <pre>
    //     myUsers.fetchData({ userId : "fred" }, "myForm.setValues(data)");
    // </pre>
    // or
    // <pre>
    //     myUsers.fetchData({ userId : "fred" }, function (dsResponse, data, dsRequest) {
    //                                              myForm.setValues(data);
    //                                            });
    // </pre>
    // <P>
    // Note that if the request encounters a low-level error (such as 500 server error), by
    // default the callback will <b>not</b> be fired, instead, +link{DataSource.handleError()}
    // is called to invoke the default system-wide error handling.  Set
    // +link{rpcRequest.willHandleError,willHandleError}:true to have your callback invoked
    // regardless of whether there are errors, however, make sure your callback properly
    // handles malformed responses when +link{dsResponse.status} is non-zero.
    //
    // @treeLocation Client Reference/Data Binding/DataSource
    // @visibility external
    //<

//> @class DSResponse
// Response sent by the server in response to a +link{DSRequest,DataSource request}.  Contains
// all the properties available on the basic +link{class:RPCResponse}, in addition to the
// properties listed here.
//
// @inheritsFrom RPCResponse
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

//> @attr dsResponse.clientContext (Object : null : R)
//
// The +link{dsRequest.clientContext} object as set on the +link{DSRequest}.
//
// @see dsRequest.clientContext
// @see rpcResponse.clientContext
//
// @visibility external
//<

//> @attr dsResponse.dataSource (String : null : IR)
// The DataSource of this DSResponse.
//
// @group dsResponse
// @visibility external
//<

//> @attr dsResponse.operationType (DSOperationType : null : IR)
// The operation type of the request corresponding to this DSResponse.
//
// @group dsResponse
// @visibility external
//<

//> @attr dsResponse.data (Array of Record | RecordList : null : IR)
// For "fetch" operations, this is the array of Records fetched.  For "update", "add", and
// "remove" operations, this is typically an array containing a single Record representing
// the record that was updated, added, or removed.
//
// @group dsResponse
// @visibility external
//<

//> @attr dsResponse.status (int : : IR)
//
// Same meaning as +link{RPCResponse.status}, except DSResponses have additional error codes,
// such as +link{RPCResponse.STATUS_VALIDATION_ERROR,validation failure}.
//
// @see group:dataSourceOperations for standard return types for DataSource Operations
// @group errorHandling
// @visibility external
//<

//> @attr dsResponse.queueStatus (int : : IR)
// An extra property of each DSResponse to a queued request that indicates whether the queue
// as a whole succeeded.  A queueStatus of +link{RPCResponse.STATUS_SUCCESS}, or 0, indicates
// that the queue succeeded whereas a queueStatus of +link{RPCResponse.STATUS_FAILURE}, or -1,
// indicates that the queue failed.
//
// <p>For example, if two "update" requests are sent in a queue and the first succeeded, but
// the second failed validation, then both DSResponses' queueStatus would be -1, but the
// +link{DSResponse.status,status} of the first would be +link{RPCResponse.STATUS_SUCCESS} and
// the status of the second would be an error code such as +link{RPCResponse.STATUS_VALIDATION_ERROR}.
//
// @group errorHandling
// @visibility external
//<

//> @attr dsResponse.errors (Object : null : R)
// Server-side validation errors for an attempted "update" or "add" operation, as a
// JS Object where each property name is a field name from the record and each property
// value contains error information.
// <P>
// To extract just the simple error strings for each field we recommend passing this
// object to +link{dataSource.getSimpleErrors()}
// <P>
// The Java API DSResponse.addError(fieldName, errorMessage) is used to send server-side
// errors to the client.  See the Java Server Reference for details.
//
// @see dataSource.handleError()
// @group errorHandling
// @visibility external
//<

//> @attr dsResponse.startRow (number : null : R)
// Starting row of returned server results, when using paged result fetching
// <p>
// Note that startRow and endRow are zero-based - the first record is row zero.
//
// @group paging
// @visibility external
//<

//> @attr dsResponse.endRow (number : null : R)
// End row of returned server results, when using paged result fetching
// <p>
// Note that startRow and endRow are zero-based - the first record is row zero.
//
// @group paging
// @visibility external
//<

//> @attr dsResponse.totalRows (number : null : R)
// Total number of rows available from the server that match the current filter criteria,
// when using paged result fetching.
//
// @group paging
// @visibility external
//<

//> @attr dsResponse.invalidateCache (boolean : null : R)
// Optional flag that can be set by the server to force ResultSets to drop any caches of
// records from the DataSource that was the target of the operation.
//
// @group cacheSync
// @visibility external
//<

//> @attr dsResponse.cacheTimestamp (number : null : R)
// Optional number sent by the server to allow cache staleness detection.
// <P>
// If sent by the server, the cacheTimestamp will be sent along with any fetch DSRequests
// originated by ResultSets who received a DSResponse with a
// <code>cacheTimestamp</code> set.
//
// @group cacheSync
//<

//> @attr dsResponse.offlineTimestamp (number : null : R)
// Timestamp (millisecond value) to indicate when this dsResponse was cached in
// +link{class:Offline,offline storage}.  Not applicable if the response has never been
// stored offline.
// @group offlineGroup
// @visibility external
//<

//> @attr dsResponse.fromOfflineCache (boolean : null : R)
// If set, indicates that this response came from the offline cache, not the server.  This
// flag is the only reliable way for application code to determine the source of a response.
// @group offlineGroup
// @visibility external
//<

//> @attr dsResponse.httpHeaders (Object : null : R)
// HTTP headers returned by the server as a map from header name to header value.
// <P>
// Headers are available only when the default +link{RPCTransport} "xmlHttpRequest" is in use,
// and browsers may limit access to headers for cross-domain requests or in other
// security-sensitive scenarios.
//
// @visibility external
//<

// --------------------------------------------------------------------------------------------

//> @class DSRequest
// Request sent to the server to initiate a
// +link{group:dataSourceOperations,DataSource operation}.  All properties which are legal on
// +link{class:RPCRequest} are legal, in addition to the properties listed here.
// <P>
//
//
//
// @inheritsFrom RPCRequest
// @treeLocation Client Reference/Data Binding
// @see RPCRequest
// @visibility external
//<

//> @attr dsRequest.dataSource (String : null : IR)
// DataSource this DSRequest will act on.
// <P>
// This property is generally automatically populated, for example when calling
// +link{method:DataSource.fetchData()} the dataSource property is set to the target
// DataSource.
// @visibility external
//<

//> @attr dsRequest.operationType (DSOperationType : null : IR)
// Type of operation being performed: "fetch", "add", "remove", "update" or "custom".
// <P>
// This property is generally automatically populated, for example when calling
// <code>fetchData()</code> on a DataSource or DataBound component the operationType is
// automatically set to "fetch".  Note that "custom" operations are never generated
// automatically, they are always fired by your code.
// @visibility external
//<

//> @attr dsRequest.data  (Object or Array of Object : null : IR)
// Data, such as search criteria or an updated record, to be acted upon.  Contents differ by
// <code>operationType</code>, see +link{group:dataSourceOperations,DataSource Operations} for
// details.
// <P>
// This field is generally filled in by passing the "data" argument to methods such as
// +link{method:ListGrid.fetchData()}.
//
// @visibility external
//<

//> @attr dsRequest.callback (DSCallback : null : IR)
//
// A callback method that will be called with an instance of DSResponse, as sent by the server.
// Queuing does not affect callbacks in any way - your specified callback will be invoked for
// each DSRequest that contained a callback regardless of whether the request was sent as part
// of a queue or not.
// <P>
// Note that if the request encounters an error (such as 500 server error), by default the
// callback will <b>not</b> be fired.  Instead, +link{RPCManager.handleError()} is called to
// invoke the default system-wide error handling.  Set +link{RPCRequest.willHandleError,willHandleError}:true to have your
// callback invoked regardless of whether there are errors; however, make sure your callback
// properly handles malformed responses when +link{RPCResponse.status} is non-zero.  See the
// +link{group:errorHandling,error handling overview} below for more details.
//
// @group errorHandling
// @visibility external
//<

//> @attr dsRequest.startRow (number : null : IR)
// Starting row of requested results, used only with fetch operations.  If unset, 0 is assumed.
// <p>
// Note that startRow and endRow are zero-based, inclusive at the beginning and exclusive at
// the end (like substring), so startRow: 0, endRow: 1 is a request for the first record.
//
// @group paging
// @visibility external
//<

//> @attr dsRequest.endRow (number : null  : IR)
// End row of requested results, used only with fetch operations.
// <p>
// Note that startRow and endRow are zero-based, inclusive at the beginning and exclusive at
// the end (like substring), so startRow: 0, endRow: 1 is a request for the first record.
//
// @group paging
// @visibility external
//<

//> @attr dsRequest.sortBy (String | Array of String or Array of SortSpecifiers : null : IR)
// Fieldname to sortBy, prefixed with optional "-" indicating descending sort.  For example, to
// sort by the field "userName" in ascending order, set <code>sortBy</code> to just
// "userName".  For descending sort on "userName", set <code>sortBy</code> to "-userName".
// <P>
// To sort by multiple fields, an Array of Strings is also supported.  For example, to sort by
// the field "department" in ascending order, followed by the field "userName" in descending
// order, set <code>sortBy</code> to:
// <P>
// <code>[ "department", "-userName" ]</code>
// <P>
// Additionally, this property supports an array of +link{SortSpecifier} objects.  Setting
// <code>sortBy</code> to the following SortSpecifier array results in the same multi-level
// sort mentioned above:
// <P>
// <code>
// [<br>
// &nbsp;&nbsp;{ property: "department", direction: "ascending" },<br>
// &nbsp;&nbsp;{ property: "userName", direction: "descending" }<br>
// ]
// </code>
//
// @visibility external
//<

//> @attr dsRequest.summaryFunctions (Object<String,SummaryFunction> : null : IR)
// A mapping from field names to +link{type:SummaryFunction,summary functions} to be applied
// to each field.
// <p>
// Valid only for an operation of type "fetch".  See the
// +link{group:serverSummaries,Server Summaries overview} for examples of usage.
//
// @group serverSummaries
// @see dsRequest.groupBy
// @visibility external
//<

//> @attr dsRequest.groupBy (Array of String : null : IR)
// List of fields to group by when using +link{group:serverSummaries,server-side summarization}.
// <p>
// Valid only for an operation of type "fetch".  See the
// +link{group:serverSummaries,Server Summaries overview} for details and examples of usage.
//
// @group serverSummaries
// @see dsRequest.summaryFunctions
// @visibility external
//<

//> @attr dsRequest.outputs (String : null : IR)
// The list of fields to return in the response, specified as a comma-separated string (eg,
// <code>"foo, bar, baz"</code>).  You can use this property to indicate to the server that
// you are only interested in a subset of the fields that would normally be returned.
// <p>
// Note that you cannot use this property to request a <em>superset</em> of the fields that
// would normally be returned, because that would be a security hole.  It is possible to
// configure individual +link{class:operationBinding}s to return extra fields, but this must
// be done in the server's +link{class:DataSource} descriptor; it cannot be altered on the fly
// from the client side.
//
// @see attr:operationBinding.outputs
// @see attr:dsRequest.additionalOutputs
// @visibility external
//<

//> @attr dsRequest.additionalOutputs (String : null : IRA)
// For fetch, add or update operation, an optional comma separated list of fields to fetch from another,
// related DataSource.
// <P>
// Fields should be specified in the format
// <code>"localFieldName!relatedDataSourceID.relatedDataSourceFieldName"</code>.
// where <code><i>relatedDataSourceID</i></code> is the ID of the related dataSource, and
// <code><i>relatedDataSourceFieldName</i></code> is the field for which you want to
// fetch related values. The returned field values will be stored on
// the data returned to the client under the specified <code><i>localFieldName</i></code>.
// Note that this will be applied in addition to any specified +link{dsRequest.outputs}.
// <P>
// Note that as with +link{dataSourceField.includeFrom}, the related dataSource must be
// linked to the primary datasource via a foreignKey relationship.
// <P>
// Note additionalOutputs sent in request from the browser can be completely disabled in
// +link{group:server_properties,server.properties} by setting
// <code>datasource.allowClientAdditionalOutputs</code>:
// <pre>
//     datasource.allowClientAdditionalOutputs: false
// </pre>
// In this case +link{dsRequest.additionalOutputs} sent from the browser will be cleared before
// executing request. Note that programatically configured additionalOutputs are always allowed, but
// you can't modify them from within a DMI method, so the only way to execute a request
// with additionalOutputs that differ from what was sent by the client is to create a new DSRequest
//
// @visibility crossDS
//<

//> @attr dsRequest.generateRelatedUpdates (boolean : null : IRW)
// Specifies should related updates have to be generated.
// If not set (or set to <code>null</code>) then related updates will be generated only for
// "add" and "update" operations. This property has to be explicitly set to <code>true</code>
// to generate related updates for "remove" operation.<p/>
// This functionality loads related objects from database thus affecting operation performance.
// For "add" and "update" operations related objects are loaded anyway and performance impact is minimal.
// Simple "remove" operation does not need to load related objects. Depending on database structure
// performance impact can be significant if this property is set to <code>true</code>.
// <P>
// Note this feature works only with Hibernate/JPA data sources, see
// +link{group:jpaHibernateRelations,JPA & Hibernate Relations} for instructions how to
// set up relations. Table below uses "country -&gt; cities" sample data model.
// <P>
// <table border=1 class="normal">
//   <tr>
//     <td width="20%"><b>Relation and Operation type</b></td>
//     <td width="40%"><b>Loading complete related objects</b></td>
//     <td width="40%"><b>Loading related IDs</b></td>
//   </tr>
//   <tr>
//     <td><i>Many-to-one (cities -&gt; country): ADD/UPDATE</i></td>
//     <td>If operation affected country, for example new city added with existing
// countryId, then relatedUpdate is generated. Otherwise if city is added or updated without
// countryId set, relatedUpdate is not generated.<br>
// Note that if provided countryId does not exist, it is created.</td>
//     <td>Same as with complete related objects, except if provided countryId does not exist,
// then it is <i>not</i> created, but reset to NULL.</td>
//   </tr>
//   <tr>
//     <td><i>Many-to-one (cities -&gt; country): REMOVE</i></td>
//     <td colSpan=2>Removes record, depending on setting generates or not relatedUpdate for parent record.
// For example if city record is removed and countryId is sent to the server in remove request, then
// country record will be generated in relatedUpdates.</td>
//   </tr>
//   <tr>
//     <td><i>One-to-many (country -&gt; cities): ADD/UPDATE</i></td>
//     <td>If add or update operation provides value sets for cities as well as for country, then
// cities are created/updated if necessary and relatedUpdates are generated.<br>
// Note that all fields in cities value sets can be sent to server.
//     <td>Same as with complete related objects, except you can only sent primary key values for cities.</td>
//   <tr>
//     <td><i>One-to-many (country -&gt; cities): REMOVE</i></td>
//     <td colSpan=2>Removes country, depending on setting returns or not relatedUpdates for the cities of removed country,
// which can be either REMOVE operations of all cities if cascade enabled, or UPDATE operations setting countryId=null to
// all cities if cascade is disabled</td>
//   </tr>
// </table>
// <P>
// Note that Many-to-Many works the same way as One-to-Many.
//
// @visibility external
//<

//> @type TextMatchStyle
// For "fetch" operations, how simple search criteria should be interpreted for text fields
// (and to some extent numeric fields, see the notes below).  For greater flexibility, use
// +link{AdvancedCriteria}.
// @value "exact" case-insensitive exact match ("foo" matches "foo" and "FoO", but not "FooBar")
// @value "exactCase" case-sensitive exact match ("foo" matches only "foo")
// @value "substring" case-insenstive substring match ("foo" matches "foobar" and "BarFoo")
// @value "startsWith" case-insensitive prefix match ("foo" matches "FooBar" but not "BarFoo")
// <p>
// textMatchStyle behavior is consistent across the client and all three of the built-in
// server DataSource implementations - SQL, Hibernate and JPA - to the maximum extent that is
// practical.  Known inconsistencies, some of which can be tweaked with configuration settings,
// are described below<ul>
// <li>textMatchStyle can be disabled for any individual field with the
// +link{dataSourceField.ignoreTextMatchStyle} declaration.  This behavior is consistent
// across client and server implementations, with the proviso that case-sensitive behavior
// is inherently inconsistent in the server-side implementations (see below for a more
// detailed discussion of this)</li>
// <li>textMatchStyle does not apply to boolean or temporal (date, datetime, time)
// +link{dataSourceField.type,field types}.  Such fields are always checked for equality.  If
// you have a need for more complex matching, such as date-range checking, use AdvancedCriteria</li>
// <li>Critiera values for temporal fields <b>must</b> be real dates (Javascript <code>Date</code>
// objects on the client, instances of <code>java.util.Date</code> on the server).  Attempting
// to match against a string or any other type of value will result in an "always fails"
// clause being generated.  This behavior is how the client code has always worked, but is a
// change for the server-side DataSources, in the interests of consistency.  The previous
// behavior was:<ul>
//   <li>SQLDataSource used to attempt to match the date column to the criteria value provided;
//   if that value was an appropriately-formatted string, that attempt <i>might</i> have
//   worked, but it would be database-specific.  If you have an application that depends on
//   this old behavior, you can switch it back on by adding the following to your
//   <code>server.properties</code> file:
//   <code>sql.temporal.fields.require.date.criteria: false</code></li>
//   <li>HibernateDataSource and JPADataSource used to skip such clauses altogether, which
//   results in the opposite behavior (there is an implied "always succeeds" clause, because
//   the clause is not present in the query at all).  If you have an application that depends on
//   this old behavior, you can switch it back on by adding the following to your
//   <code>server.properties</code> file:
//   <code>jpa.temporal.fields.require.date.criteria: false</code> or
//   <code>hibernate.temporal.fields.require.date.criteria: false</code>, as appropriate</li>
// </ul></li>
// <li>textMatchStyle does not apply to fields used in the joins generated to satisfy
// +link{dataSourceField.includeFrom,includeFrom} declarations or entity relations in
// Hibernate or JPA.  Again, such comparisons are always for equality</li>
// <li>textMatchStyle is applied to numeric fields <b>only</b> if the supplied criteria value
// is a string.  So for a numeric field "myNumber", we will match 10, 100, 110 and 1000 if a
// criteria value of "10" (the quoted string) is supplied, but we will only match 10 if a
// criteria value of 10 (the actual number) is supplied<ul>
//   <li>This is a change in behavior for the server-side DataSource implementations, in the
//   interests of consistency with the client behavior.  Previously, all three of the built-in
//   DataSource implementations applied textMatchStyle to numeric fields regardless of the
//   criteria value's type.  If you have an application that depends on this old behavior,
//   you can switch it back on by adding the following to your <code>server.properties</code>
//   file: <code>sql.substring.numeric.criteria: true</code> or
//   <code>hibernate.substring.numeric.criteria: true</code> or
//   <code>jpa.substring.numeric.criteria: true</code>, as appropriate</li>
// </ul></li>
// <li>"exactCase" matching cannot be made consistent across all database providers because
// each database has its own idea of whether case-sensitive matching should be the default,
// and how to switch it on and off (if that is even possible)<ul>
//   <li>Microsoft SQL Server must have case-sensitive matching enabled by setting a
//   case-sensitive collation when the database is created (there are more fine-grained
//   options, but we do not recommend them because of the scope for further inconsistency,
//   where "exactCase" works correctly on table A but not on table B).  See
//   <a href=http://msdn.microsoft.com/en-us/library/ms175835.aspx>MSDN</a> for details</li>
//   <li>HSQLDB has case sensitivity set with the <code>SET IGNORECASE</code> directive -
//   <a href=http://www.hsqldb.org/doc/guide/ch09.html#set_ignorecase-section>see here</a></li>
//   <li>MySQL is case-insensitive, but since it provides an easy way to do per-query,
//   case-sensitive matching in SQL, we work around it for that database, but <b>only</b> in
//   SQL DataSources</li>
// </ul></li>
// <li>If you are using Hibernate or JPA, whether "exactCase" works as expected depends
// entirely on the underlying database.  Unlike the SQL DataSource, where we have fine-grained
// control of the SQL that the database executes, we are constrained by what the ORM will
// allow.  This prevents us from compensating for database quirks, like we do with MySQL's
// case-insensitivity in the SQL DataSource</li>
// </ul>
//
// @visibility external
//<

//> @attr dsRequest.textMatchStyle (TextMatchStyle: null : IR)
// For "fetch" operations, how search criteria should be interpreted for text fields: one of
// "exact" for exact match, "exactCase" for case-sensitive exact match, "startsWith" for
// matching at the beginning only, or "substring" for substring match.  All
// <code>textMatchStyle</code> settings except "exactCase" are case-insensitive; use
// +link{AdvancedCriteria} for greater control over matching.
// <p>
// This property defaults to the value of +link{dataSource.defaultTextMatchStyle} if it is not
// explicitly provided on the <code>DSRequest</code>.  Note, however, that DSRequests issued
// by +link{ListGrid}s and other +link{DataBoundComponent,components} will generally have a
// setting for textMatchStyle on the component itself
// (see +link{ListGrid.autoFetchTextMatchStyle}, for example).
// <p>
// This setting is respected by the built-in server-side connectors for SQL, JPA and Hibernate.
// A custom server-side DataSource implementation should generally respect this flag as well,
// or server-side filtering will not match client-side filtering, which will require
// +link{resultSet.useClientFiltering,disabling client-side filtering}, a huge performance
// loss.
//
// @visibility external
//<

//> @attr dsRequest.oldValues (Record : null : IR)
// For an <code>update</code> or <code>remove</code> operation, the original values from the
// record that is being updated or removed.  <code>oldValues</code> is automatically added to
// DSRequests submitted by DataBound Components.  Available on the server via
// <code>DSRequest.getOldValues()</code>.
// <P>
// The server can compare the <code>oldValues</code> to the most recent stored values in order
// to detect that the user was looking at stale values when the user submitted changes (NOTE:
// this means of detecting concurrent edit is sometimes called "optimistic concurrency" or
// "long transactions").
// <P>
// In applications where a policy of "last update wins" is not appropriate when updating
// certain fields, special UI can be shown for this case.  For example, on detecting concurrent
// edit, the server may send back a special <code>dsResponse.status</code> code that the client
// application detects, offering the user a choice of proceeding with the operation,
// discarding edits, or reconciling new and old values in a special interface.
//
// @visibility external
//<

//> @attr dsRequest.componentId (String : null : IR)
// For requests submitted by a +link{DataBoundComponent}, the +link{canvas.ID} of the
// submitting component.
// <P>
// This ID will be present for operations including automatic saves by a ListGrid
// +link{group:editing,during editing}, or calls to
// +link{dynamicForm.saveData,form.saveData()}.  It will not be present for a direct
// call to a DataSource method such as +link{dataSource.fetchData()}.
// <P>
// Note this is the component's <b>String</b> ID - you can retrieve the component itself
// via +link{Canvas.getById()}.
// <P>
// This property should be used for debugging purposes only - do not use it to trigger
// differences in server-side behavior, instead, use +link{dsRequest.operationId} because only
// <code>operationId</code> is considered when assessing
// +link{group:dsRequestEquivalence,request equivalence}.
//
// @visibility external
//<

//> @attr dsRequest.operationId (String : null : IR)
// When a +link{interface:DataBoundComponent} sends a DSRequest, the
// <code>dsRequest.operationId</code> will be automatically picked up from the
// <code>fetchOperation</code>, <code>addOperation</code>, etc properties of the
// DataBoundComponent.
// <P>
// The <code>operationId</code> serves as an identifier that you can use to create
// variations on the 4 basic DataSource operations that are used by different components in
// different parts of your application.  For example, you may be using a standard
// <code>fetch</code> operation in one part of your application, however on another screen you
// want to perform a <code>fetch</code> operation on the same DataSource but interpret search
// criteria differently (eg full text search).
// <P>
// If you declare more than one +link{OperationBinding} for the same
// +link{operationBinding.operationType}, you can specify an <code>operationId</code>
// +link{operationBinding.operationId,on the operationBinding} which will cause that
// operationBinding to be used for dsRequests containing a matching <code>operationId</code>.
// This allows all the possible settings of an <code>operationBinding</code>, including
// +link{operationBinding.wsOperation,wsOperation} or +link{group:dmiOverview,DMI} settings, to be switched on a
// per-component or per-request basis.
// <P>
// For example, by setting the <code>fetchOperation</code> on a particular ListGrid, you could
// cause it to invoke a different server method via DMI, different
// +link{operationBinding.dataURL,dataURL} or different
// +link{operationBinding.wsOperation,web service operation}.
// <P>
// The <code>operationId</code> can also be directly received by the server in order to affect
// behavior.  When using the SmartClient Server, <code>operationId</code> can be accessed via
// dsRequest.getOperationId().  The +link{RestDataSource} will also send the
// <code>operationId</code> to the server as part of the
// +link{restDataSource.metaDataPrefix,request metadata}.
// <P>
// Note that if you +link{dataSource.fetchData,manually invoke} a DataSource operation, you can
// also specify operationId via the <code>requestProperties</code> parameter.
// <P>
// Note that the <code>operationId</code> has special significance in terms of whether two
// DSRequests are considered equivalent for caching and synchronization purposes - see
// +link{group:dsRequestEquivalence}.
//
// @group operations
// @visibility external
//<


//> @groupDef dsRequestEquivalence
// Various subsystems have a need to compare DataSource requests and understand if
// they are equivalent or affect the same data (examples include
// +link{ResultSet,automatic cache synchronization} and
// +link{DataSource.useOfflineStorage,offline caching and synchronization}).
// <P>
// Aside from basic properties that would clearly make two DSRequests non-equivalent
// (dataSource, operationType and data, as well as sortBy, startRow, endRow and textMatchStyle
// for a "fetch"), +link{dsRequest.operationId} is the only property that will cause two
// DSRequests to be considered distinct (non-equivalent) requests.
// <P>
// Bearing this in mind, the best practice is:
// <ul>
// <li> everything that will be treated as criteria or as values on the server side should be
// part of +link{dsRequest.data}.  Do not "smuggle" data that will ultimately be used as
// criteria or values in other dsRequest properties, such as
// +link{rpcRequest.params,HTTP parameters}.
// <li> use +link{dsRequest.operationId} as the sole piece of information in the request that
// modifies how the request as a whole is executed.  If two or more pieces of information are
// required, combine or encode them into a single operationId String.  If this becomes awkward
// because there are many operation variants, consider including additional fields in
// +link{dsRequest.data} instead.
// </ul>
//
// @visibility external
//<

//> @attr dsRequest.requestId (String : varies : RA)
// Automatically generated unique ID for this request. This ID will be required by developers
// making use of the +link{type:DSProtocol,"clientCustom" dataProtocol}.
// @visibility external
//<

//> @attr dsRequest.clientContext (Object : {} : IRW)
// An object to be held onto for the duration of the DSRequest turnaround to track
// application-specific context.
// <P>
// When a DataSource request completes, the <code>clientContext</code> is available in the
// +link{type:DSCallback} as <code>dsResponse.clientContext</code>.  The
// <code>clientContext</code> is never sent to the server.
// <br>
// The <code>clientContext</code> is useful for holding onto state that will be used when the
// +link{type:DSCallback} fires, such as the name of a component that will receive the
// returned data.
//
// @see DSResponse.clientContext
// @see RPCRequest.clientContext
//
// @visibility external
//<

//> @attr dsRequest.parentNode (TreeNode : null : R)
// For advanced use in integrating trees that
// +link{TreeGrid.loadDataOnDemand,load data on demand} with web services,
// <code>parentNode</code> is automatically set in "fetch" DSRequests issued by a
// databound TreeGrid that is loading children for that <code>parentNode</code>.
// <P>
// This is sometimes needed if a web service requires that additional properties beyond the
// ID of the parentNode must be passed in order to accomplished level-by-level loading.
// A custom implementation of +link{dataSource.transformRequest()} can access
// dsRequest.parentNode and add any such properties to +link{dsRequest.data}.
// <P>
// <code>parentNode</code> will also be automatically set by a TreeGrid performing databound
// reparenting of nodes, as implemented by +link{treeGrid.folderDrop}.
// <P>
// This property can only be read.  There is no meaning to setting this property
// yourself.
//
// @visibility external
//<

//> @attr dsRequest.resultTree (ResultTree : null : R)
// For advanced use in integrating trees that
// +link{resultTree.loadDataOnDemand,load data on demand} with web services,
// the ResultTree that issued this "fetch" DSRequest is automatically made available as the
// <code>resultTree</code> property.
// <P>
// This property can only be read.  There is no meaning to setting this property
// yourself.
//
// @visibility external
//<

//> @attr dsRequest.keepParentsOnFilter (Boolean : null : IRW)
// This property is for advanced use in integrating trees that
// +link{TreeGrid.loadDataOnDemand,load data on demand} using data paging.
// When this flag is set, a server fetch operation is expected to return all of the tree nodes
// that either match the provided criteria <b>or</b> have one or more children that match the
// criteria.
// <p>
// A ResultTree with +link{resultTree.fetchMode,fetchMode:"paged"} and with
// +link{resultTree.keepParentsOnFilter,keepParentsOnFilter} enabled will automatically set
// this property to <code>true</code> on all DSRequests that it sends to the server.
// <p>
// Currently, no built-in server-side connectors (SQL, JPA, Hibernate) implement support for
// the keepParentsOnFilter flag.
//
// @group treeDataBinding
// @visibility external
//<


//> @attr dsRequest.resultSet (ResultSet : null : R)
// For advanced use in integrating dataset paging with web services,
// the ResultSet that issued this "fetch" DSRequest is automatically made available as the
// <code>resultSet</code> property.
// <P>
// This property can only be read.  There is no meaning to setting this property
// yourself.
//
// @visibility external
//<

//> @attr dsRequest.shouldUseCache (boolean : null : IRW)
// This is a per-request flag for explicitly controlling whether the cache is used (bypassing
// it when not wanted, or using it when settings would indicate otherwise). See
// +link{dataSource.cacheAllData}, +link{dataSource.cacheAllOperationId} and
// +link{dataSource.cacheAcrossOperationIds} for caching management for all requests of a
// dataSource.
//
// @visibility external
//<



//> @attr dsRequest.useFlatFields (Boolean : null : IR)
// When <code>useFlatFields</code> is set for a request to be sent to a WSDL web service, when
// creating the input XML message to send to the web service, properties in
// +link{data,request.data} will be used as the values for XML elements of the same name, at
// any level of nesting.
// <P>
// <code>useFlatFields</code> allows you to ignore gratuitous XML message structure, such as
// extra levels of nested elements, and provides some insulation against changes in the
// required structure of the input message.
// <P>
// For example, given this input message:
// <pre>
// &lt;FindServices&gt;
//     &lt;searchFor&gt;search text&lt;/searchFor&gt;
//     &lt;Options&gt;
//         &lt;caseSensitive&gt;false&lt;/caseSensitive&gt;
//     &lt;/Options&gt;
//     &lt;IncludeInSearch&gt;
//         &lt;serviceName&gt;true&lt;/serviceName&gt;
//         &lt;documentation&gt;true&lt;/documentation&gt;
//         &lt;keywords&gt;true&lt;/keywords&gt;
//     &lt;/IncludeInSearch&gt;
// &lt;/FindServices&gt;
// </pre>
// If <code>useFlatFields</code> were <b>not</b> set, in order to fill out this message
// correctly, <code>request.data</code> would need to be:
// <pre>{
//    searchFor: "search text",
//    Options : {
//        caseSensitive: false,
//    },
//    IncludeInSearch : {
//        serviceName: true,
//        documentation : true,
//        keywords : true
//    }
// }</pre>
// However if useFlatFields were set, <code>request.data</code> could be just:
// <pre>{
//    searchFor: "search text",
//    caseSensitive: false,
//    serviceName: true,
//    documentation : true,
//    keywords : true
// }</pre>
// <code>useFlatFields</code> is often set when the input data comes from a +link{DynamicForm}
// to avoid the cumbersome and fragile process of mapping input fields to an XML structure.
// <P>
// +link{operationBinding.useFlatFields} can also be set to cause <b>all</b> dsRequests of a
// particular type to <code>useFlatFields</code> automatically.
// <P>
// For +link{DataBoundComponent,DataBoundComponents},
// +link{DataBoundComponent.useFlatFields,component.useFlatFields} can be set use "flattened"
// binding to fields of a WSDL message or XML Schema.
// <P>
// Note that <code>useFlatFields</code> is not generally recommended for use with XML input
// messages where multiple simple type fields exist with the same name, however if used in this
// way, the first field to use a given name wins.  "first" means the first field encountered in a
// depth first search.  "wins" means only the first field will be populated in the generated
// XML message.
//
// @group flatFields
// @visibility xmlBinding
//<

//> @attr dsRequest.useFlatHeaderFields (boolean : null : IRW)
// Cause the +link{dsRequest.useFlatFields,useFlatFields} XML serialization behavior to be used
// for <b>all</b> soap headers in the request.  See also +link{dsRequest.headerData}.
//
// @group flatFields
// @visibility xmlBinding
//<

//> @attr dsRequest.headerData (Object : null : IRW)
// For DataSources using SOAP messaging with a WSDL web service, data to be serialized to form
// SOAP headers, as a map from the header part name to the data.  See
// +link{WSRequest.headerData} for more information.
// <P>
// SOAP headers typically contain request metadata such as a session id for authentication, and
// so <code>dsRequest.headerData</code> is typically populated by
// +link{dataSource.transformRequest()}, or, for data that applies to every request sent to the
// server, by +link{webService.getHeaderData()}.
// <P>
// Note that this only applies to SOAP headers. General HTTP headers for requests may be
// modified using +link{RPCRequest.httpHeaders}.
//
// @visibility xmlBinding
//<

//> @type ExportFormat
// One of the supported formats for data-export.  If you are doing a
// +link{listGrid.exportClientData(),client export} to one of the native spreadsheet
// formats (xls or ooxml), we also export +link{object:Hilite,hilite-based} coloring.  So, if
// Hilites are causing a particular cell to be rendered as green text on a blue background,
// the corresponding cell in the exported spreadsheet document will also be colored that way.
//
// @value "xml"    Export data as XML records
// @value "json"   Export data as JSON objects
// @value "csv"    Export data in comma-separated format
// @value "xls"    Export data in native Microsoft Excel 97 format
// @value "ooxml"  Export data in native Microsoft Excel 2007 format (also called XLSX)
//
// @serverDS allowed
// @visibility external
//<

//> @type ExportImageFormat
// One of the supported formats for image export.
//
// @value "png"    Export as PNG
// @value "jpeg"   Export as JPEG
//
// @serverDS allowed
// @visibility external
//<

//> @type ExportDisplay
// Method to use for displaying the exported data.
//
// @value "download"    Show the Save As dialog and download the file
// @value "window"      Show the data in a new browser window
// @value "return"      Return the data for further programmatic processing in the browser
//
// @serverDS allowed
// @visibility external
//<

//> @type LineBreakStyle
// The style of line-breaks to use when exporting data
//
// @value "default"    Use the default line-break style of the server OS
// @value "unix"       Use UNIX-style line-breaks (LF only)
// @value "mac"        Use MAC-style line-breaks (CR only)
// @value "dos"        Use DOS-style line-breaks (both CR & LF)
//
// @serverDS allowed
// @visibility external
//<

//> @attr dsRequest.exportResults (Boolean : false : IR)
// When set, causes the results of the DSRequest to be exported to a file, whose name
// and format are indicated by +link{dsRequest.exportFilename} and
// +link{dsRequest.exportAs} respectively.  When no exportFilename is provided, the default is
// <i>Results.csv</i> and the default value of exportAs is <i>csv</i>.
// <P>
// The export field-list can also be configured, see +link{dsRequest.exportFields}.  Formats
// for exported date and numeric are controlled by several settings - see
// +link{group:exportFormatting} for an overview.
// <P>
// Once the operation completes, +link{dsRequest.exportDisplay} specifies whether the exported
// data should be downloaded to the file-system or displayed in a new window.  The default value
// of exportDisplay is "download" which displays the Save As dialog.  See +link{ExportDisplay}
// for more information.
// <P>
// You can configure the style of +link{LineBreakStyle, line-breaks} to use when
// generating the output, the +link{dsRequest.exportDelimiter, delimiter} to use when exporting
// to CSV and the +link{dsRequest.exportTitleSeparatorChar, separator-character} to use in
// field-titles when exporting to XML.
// <P>
// Additionally, you can output arbitrary text before and after the exported data by setting
// +link{dsRequest.exportHeader, exportHeader} and +link{dsRequest.exportFooter, exportFooter}.
// <P>
// Note that for security reasons, an export initiated using dsRequest properties does not
// provide support for JSON format (see
// <a href="http://forums.smartclient.com/showthread.php?t=235">this post</a> for more detail).
// <P>
// As well as setting dsRequest.exportResults and related properties, exports can be initiated
// in two other ways, via +link{OperationBinding}s and via custom server code which sets
// export-related properties on the +link{DSResponse}.  Both of those methods support exporting
// to JSON format.
// <P>
// <b>Format Examples</b>
// XML format
// <pre>
//     &lt;List&gt;
//         &lt;Object&gt;
//             &lt;id&gt;10101&lt;/id&gt;
//             &lt;displayName&gt;Record 10101&lt;/displayName&gt;
//         &lt;/Object&gt;
//    &lt;/List&gt;
// </pre>
// JSON Format
// <pre>
//     [
//         { id: 10101, displayName: "Record 10101" }
//     ]
// </pre>
// CSV Format
// <pre>
//     id,displayName
//     10101,"Record 10101"
// </pre>
//
// @visibility external
//<

//> @attr dsRequest.exportAs (ExportFormat : "csv" : IR)
// The format in which the data should be exported.  See +link{ExportFormat} for more
// information.
//
// @visibility external
//<

//> @attr dsRequest.exportFilename (String : null : IR)
// The name of the file to save the exported data into.  If
// +link{dsRequest.exportToFilesystem,exportToFilesystem} is set, this is the name of the
// file the server creates on its filesystem.  If +link{dsRequest.exportToClient,exportToClient}
// is set, this is the filename that will appear to the browser.
// <p>
// If the exportFilename that you specify does not include an extension, one will be added to
// it based on the +link{type:ExportFormat} specified by +link{dsRequest.exportAs}.
//
// @see dsRequest.exportPath
// @visibility external
//<

//> @attr dsRequest.exportImageFormat (ExportImageFormat : "png" : IR)
// The image format in which the SVG graphic should be exported.
//
// @visibility external
//<

//> @attr dsRequest.exportImageQuality (float : null : IR)
// If exporting in +link{ExportImageFormat,JPEG format}, the output JPEG quality level.
// This is a number from 0 to 1, with 1 representing the best quality and 0 representing the
// least quality but smallest file size.
//
// @visibility external
//<

//> @attr dsRequest.exportPath (String : null : IR)
// If +link{dsRequest.exportToFilesystem,exportToFilesystem} is set, optionally specifies a
// path to use when saving the file.  This path is relative to the default export path, which
// is set using the +link{group:server_properties,server.properties} setting <code>export.location</code>; this
// is the project webRoot by default.  For example, with the default setting of
// <code>export.location</code>, an <code>exportPath</code> of <code>"shared/ds"</code> and
// an +link{dsRequest.exportFilename,exportFilename} of <code>"exportedData.csv"</code>,
// SmartClient Server would export to file <code>$webRoot/shared/ds/exportedData.csv</code>.
// <p>
// If you do not specify this property, SmartClient Server will export to the file indicated
// by <code>exportFilename</code> directly in the default export location.
// <p>
// This property is only applicable when +link{dsRequest.exportToFilesystem,exportToFilesystem}
// is set.
//
// @see dsRequest.exportFilename
// @visibility external
//<

//> @attr dsRequest.exportToFilesystem (Boolean : false : IR)
// If set, SmartClient Server will export data to a file on the <b>server</b> filesystem.  The
// file we export to is determined by the +link{dsRequest.exportFilename,exportFilename} and
// +link{dsRequest.exportPath,exportPath}.  Note that filesystem exports are disabled by
// default, for security reasons.  To enable them, set <code>export.allow.filesystem</code> to
// true in your <code>server.properties</code> file.  If you enable filesystem exports, you
// should also consider setting a default export path, as described in the
// +link{exportPath,exportPath} documentation.
// <p>
// Note that it is perfectly valid to specify both this property and
// +link{dsRequest.exportToClient,exportToClient}; in this case the data is both exported to a
// file on the server filesystem <em>and</em> downloaded to the client.  If you specify
// <em>neither</em> property, the export no-ops.
// <p>
// It is possible to redirect the filesystem export to make use of an <code>OutputStream</code>
// you provide.  You use this when you want to make some use of the export document other than
// writing it to a disk file - for example, attaching it to an email or writing it to a
// database table.  See the server-side Javadocs for <code>DSRequest.setExportTo()</code>.
// @visibility external
//<

//> @attr dsRequest.exportToClient (Boolean : true : IR)
// If set, SmartClient Server will export data back to the client, either as a file download
// or as content in a new browser window, depending on the setting of
// +link{dsRequest.exportDisplay,exportDisplay}.
// <p>
// Note that it is perfectly valid to specify both this property and
// +link{dsRequest.exportToFilesystem,exportToFilesystem}; in this case the data is both
// exported to a file on the server filesystem, and downloaded to the client.  If you specify
// <em>neither</em> property, the export no-ops.
//
// @visibility external
//<

//> @attr dsRequest.exportTitleSeparatorChar (String : null : IR)
// The character with which to replace spaces in field-titles when exporting to XML.
//
// @visibility external
//<

//> @attr dsRequest.exportDisplay (ExportDisplay : "download" : IR)
// Specifies whether the exported data will be downloaded as an attachment or displayed in a
// new browser window. See +link{ExportDisplay} for more information.
//
// @visibility external
//<

//> @attr dsRequest.lineBreakStyle (String : null : IR)
// The style of line-breaks to use in the exported output.  See +link{LineBreakStyle} for
// more information.
//
// @visibility external
//<

//> @attr dsRequest.exportDelimiter (String : "," : IR)
// The character to use as a field-separator in CSV exports.  The default delimiter is comma.
//
// @visibility external
//<

//> @attr dsRequest.exportShowHeaderSpanTitles (Boolean : true : IR)
// When you erxport a +link{class:ListGrid} that has +link{ListGrid.headerSpans,headerSpans},
// should headerSpans also be exported.  See +link{exportSpanTitleSeparator} for details of
// of what it means to export headerSpans to different export targets.
//
// @visibility external
//<

//> @attr dsRequest.exportSpanTitleSeparator (String : " - " : IR)
// When you export a +link{class:ListGrid} that has +link{ListGrid.headerSpans,headerSpans}
// defined and +link{dsRequest.exportShowHeaderSpanTitles} is true, the behavior is dependent
// on the export type.  Direct exports to Excel formats (both XLS and OOXML) place the
// headerSpans in merged cells in the spreadsheet, giving the same visual effect as the
// original ListGrid.  This is not possible with exports to CSV format; instead, we alter the
// exported headers so that they contain the titles of the ancestor headerSpan(s).
// <p>
// For example, if you had a field titled "Population" inside a headerSpan titled "National",
// nested inside another headerSpan titled "Demographics", that would result in the exported
// field being titled "Demographics - National - Population".
// <p>
// The <code>exportSpanTitleSeparator</code> property allows you to override the separator
// string used when constructing these amalgamated headers.
//
// @visibility external
//<

//> @attr dsRequest.exportFields (Array of String : null : IR)
// The list of field-names to export.  If provided, the field-list in the exported output is
// limited and sorted as per the list.
// <P>
// If exportFields is not provided, the exported output includes all visible fields
// from the DataSource (field.hidden=false), sorted in the order they're defined.
//
// @visibility external
//<

//> @attr dsRequest.exportHeader (String : null : IR)
// Optional text to appear at the beginning of the file.
//
// @visibility external
//<

//> @attr dsRequest.exportHeaderless (Boolean : false : IRW)
// This property allows omitting column names from CSV and Excel exports (no effect on JSON or XML exports).
//
// @visibility external
//<

//> @attr dsRequest.exportFooter (String : null : IR)
// Optional text to appear at the end of the file.
//
// @visibility external
//<

//> @attr dsRequest.exportData (Array of Record : null : IR)
// Only applies to request properties passed to +link{listGrid.exportClientData()}.
// If specified this property contains an arbitrary set of data to be exported.
// @visibility external
//<

//> @attr dsRequest.exportValueFields (boolean : null : IR)
// Only applies to request properties passed to +link{listGrid.exportClientData()}.
// Ordinarily, any fields that have a +link{ListGridField.displayField,displayField} defined
// have the value of that displayFIeld exported, rather than the underlying value in the
// +link{ListGridField.valueField,valueField}.  If you set this  property, we export both
// the underlying value and the displayField value.
// @visibility external
//<

//> @attr dsRequest.exportDatesAsFormattedString (boolean : null : IR)
// When exporting via +link{listGrid.exportClientData()} to an <code>XLS</code> or
// <code>OOXML</code> spreadsheet, forces dates to export as a string rather than a true date
// value.
// <p>
// If a date value is provided to a spreadsheet as a string, Excel or other spreadsheet
// applications may not recognize them as being date values that are valid for use in
// date-specific functions in formulas, filters, etc.
// <p>
// For this reason, the default behavior of <code>exportClientData</code> is to provide date
// values to the spreadsheet as true date values.  If
// +link{type:FormatString,Format Strings} are provided via properties like
// +link{dataSourceField.format,dataSourceField.format} these will be translated to Excel /
// OpenOffice format strings and used when generating spreadsheets.  Other formatting logic,
// such as +link{listGridField.formatCellValue,cell formatters}, will not be used since they
// cannot be automatically translated to an Excel format string.  If no translatable format
// string is available, date values will be provided to the spreadsheet with no formatter and
// the spreadsheet program's default formatting for date values will be used.
// <p>
// If <code>exportDatesAsFormattedString</code> is set to true, date fields will appear as
// strings that exactly match the formatting shown in the +link{class:DataBoundComponent}.
// As noted above, this means the spreadsheet program will not recognize the value as
// a date.
// @group exportFormatting
// @visibility external
//<

//> @type ValidationMode
// Mode of validation used for running validation on partially entered data.
//
// @value "full"    Assume provided data is complete and run all validation rules
// @value "partial" Validate only the data values that are present ignoring the
//                  absence of required fields, validators that refer to fields
//                  that are not present, and any conditional validators dependent
//                  on fields that are not present.
//
// @serverDS allowed
// @visibility external
//<

//> @attr dsRequest.validationMode (ValidationMode : "full" : IR)
// Mode of validation for entered data.
//
// @visibility external
//<

//> @attr dsRequest.pendingAdd (boolean : null : IR)
// Indicates that a validation request is being made for a record that will ultimately be
// saved with an "add" request, as opposed to an "update" request.  This context is necessary
// for some validators because the nature of the validation depends on whether we are adding
// or updating a record.  The system sets this flag when processing interim validations, such
// as those fired when +link{dynamicForm.validateOnChange} is in force.
//
// @visibility external
//<

//> @attr dsRequest.parameters (Object : null : IR)
// Parameters to pass to the server <i>in addition to</i> the standard inbound data required by
// a DataSource operation of this type.
// <p>
// Parameters are used only with custom implementations of DataSource operations; the
// SQLDataSource built-in to the ISC server ignores all parameters.  A typical use of
// parameters is to control an extra action that a custom DataSource operation supports.  For
// example, a custom "update" DataSource operation might support sending an email notification
// whenever a record is updated; parameters could control whether this notification email is
// sent, to what list of people, etc.
// <p>
// An alternative to using parameters is to use +link{method:RPCManager.startQueue()} to
// combine multiple distinct operations into one round-trip.
// <p>
// These parameters are available on the server via <code>dsRequest.getParameter()</code>.
//<



//> @attr dsRequest.streamResults (String : false : IR)
// If true, results will be streamed on the server, rather than all records being read into
// server memory at once; this approach is appropriate for retrieving or exporting large
// datasets without swamping the server.
// <p>
// Although this property can be set without any particular concerns (small datasets can be
// streamed just as readily as large ones), bear in mind that although streaming enables the
// processing of very large datasets, processing and downloading very large datasets in a
// normal client/server flow will very rarely give an acceptable user experience.  Streaming
// is of more practical use in a batch setting - for example, a disconnected
// +link{dsRequest.exportToFilesystem,export}.
// <p>
// Note that streaming requires specific server support; of SmartClient's built-in DataSource
// types, only <code>SQLDataSource</code> is able to stream results.  This property is ignored
// by other DataSource types.
// <p>
// See also the server-side documentation for <code>DSResponse</code>,
// <code>SQLDataSource</code> and <code>StreamingResponseIterator</code>.
//
// @visibility external
//<


//> @attr dsRequest.progressiveLoading (boolean : null : IRW)
// Sets +link{DataSource.progressiveLoading,progressive loading mode} for this specific
// request, overriding the OperationBinding- and DataSource-level settings.  Note that this
// setting applies only to fetch requests - it has no effect if specified on any other kind
// of request.
//
// @see dataSource.progressiveLoading
// @see operationBinding.progressiveLoading
// @group progressiveLoading
// @visibility external
//<


// ServerObject
// ---------------------------------------------------------------------------------------
//> @attr DataSource.serverObject (ServerObject : null : IR)
//
// For Direct Method Invocation (DMI) binding, declares the ServerObject to use as the default
// target for all +link{DataSource.operationBindings}.  Specifying this
// attribute in an XML DataSource stored on the server enables DMI for this DataSource.
//
// @group serverDataIntegration
// @requiresModules SCServer
// @serverDS allowed
// @visibility external
//<

//> @class ServerObject
//
// The ServerObject tells the ISC server how to find or create a server-side object involved in
// +link{group:dmiOverview,DMI} (Direct Method Invocation).
// <p>
// A ServerObject declaration appears in the XML definition of a +link{DataSource} (for
// responding to +link{DSRequest}s) or in an Application configuration file (.app.xml) for
// responding to +link{RPCRequest}s.
// <P>
// NOTE: Please take note of the points made in
// +link{group:serverDataSourceImplementation,this discussion} of caching and thread-safety
// issues in server-side DataSources.
//
// @see DMI
//
// @treeLocation Client Reference/RPC
// @requiresModules SCServer
// @serverDS only
// @visibility external
//<

//> @attr serverObject.lookupStyle ("new"|"factory"|"attribute"|"spring" : "new" : IR)
//
// Specifies the mechanism for locating the class instance on which to invoke the method.
// Valid values are as follows:
// <ul>
// <li>"spring": For use with the +externalLink{http://springframework.com,Spring framework}.
// +link{serverObject.bean} contains the name of the bean to invoke.  Which application context
// is used can be configured via web.xml (see the example web.xml in the SDK).  See also
// +link{group:serverInit} for special concerns with framework initialization when using
// Spring.
// <li>"new": A new instance of the class specified by +link{serverObject.className} will be
// created and the DMI method will be invoked on that instance (unless the specified method is
// static, in which case no instance is created, but the class specified by
// +link{serverObject.className} is still used).
// <li>"factory": A custom factory provides the class instance on which the DMI method is to be
// invoked.  In this case, +link{serverObject.className} specifies the className of the factory
// that will provide the instance on which the DMI method is to be invoked.  The class
// specified by +link{serverObject.className} must provide exactly one method named
// <code>create</code> that must return the class instance on which you wish the DMI method to
// be invoked.  Like the DMI methods, the <code>create</code> method can request a standard set of
// values as arguments.  See +link{group:dmiOverview,DMI} for a list of available values.
// <li> "attribute": The instance on which the DMI method is to be invoked is looked up in the
// scope defined by +link{serverObject.attributeScope} via the attribute name specified in
// +link{serverObject.attributeName}.
// </ul>
//
// @see serverObject.className
// @see serverObject.attributeName
// @see serverObject.attributeScope
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.className (String : null : IR)
//
// Specifies the fully-qualified class name that provides the server-side endpoint of the DMI
// (+link{serverObject.lookupStyle}:"new") or the class name of the factory that produces the
// DMI instance (+link{serverObject.lookupStyle}:"factory").
// <p>
// This is one of the values that you need to pass to +link{DMI.call} to invoke the DMI from
// the client.
// <p>
// The value of this attribute is used only for <code>"new"</code> and <code>"factory"</code>
// values of +link{serverObject.lookupStyle}.
//
// @see serverObject.lookupStyle
// @see serverObject.ID
// @see DMI.call
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.methodName (String : null : IR)
//
// Specifies the name of the method to call for operations using this ServerObject.  This is
// a DataSource-level default; you can override it for individual operations either by
// specifying the +link{operationBinding.serverMethod} attribute, or by declaring an
// operation-level serverObject that specifies a different methodName (if you specify both
// an operationBinding.serverMethod and an operation-level serverObject.methodName, the
// latter takes precedence)
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.ID (String : null : IR)
//
// You can optionally specify an ID on the ServerObject config block - in which case you can
// use that value as the "className" argument when calling +link{DMI.call}.  This allows you to
// hide the name of the server-side class used as the factory or implementer of the DMI from
// the browser as a security precaution.
//
// @see serverObject.className
// @see DMI.call
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.bean (String : null : IR)
//
// For use when +link{serverObject.lookupStyle} is <code>"spring"</code>, id of the bean to ask
// Spring to create.
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.attributeName (String : null : IR)
//
// Specifies the name of the attribute by which to look up the DMI instance.  This attribute is
// consulted only when the value of +link{serverObject.lookupStyle} is <code>"attribute"</code>.
//
// @see serverObject.attributeScope
// @see serverObject.lookupStyle
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.attributeScope ("request"|"session"|"application" : null : IR)
//
// Specifies the scope in which the DMI instance is to be looked up.  Valid values are:
// <code>"request"</code>, <code>"session"</code>, and <code>"application"</code>.  If
// <code>attributeScope</code> is left out of the <code>ServerObject</code> definition, then
// all scopes are searched in the order in which they are listed above.
// <p>
// This attribute is consulted only when the value of +link{serverObject.lookupStyle} is
// <code>"attribute"</code>.
//
// @see serverObject.attributeName
// @see serverObject.lookupStyle
//
// @serverDS only
// @visibility external
//<


//> @attr serverObject.visibleMethods (List : null : IR)
//
// When the +link{ServerObject} appears in a .app.xml file (for RPC DMI), this property
// specifies the list of methods on the ServerObject that are callable from the client.  See
// the builtin.app.xml file in the /shared/app directory of the SDK for an example of a
// visibleMethods declaration block.
//
// @see DMI
//
// @serverDS only
// @visibility external
//<


//> @attr serverObject.dropExtraFields     (Boolean : null : IR)
// By default, for DMI DSResponses, DSResponse.data is filtered on the server to just the set
// of fields defined on the DataSource.  This behavior can be overridden in several ways - see
// the overview in +link{group:dmiOverview,DMI} for details.  The value of this attribute overrides
// +link{DataSource.dropExtraFields}.
//
// @serverDS only
// @visibility external
//<


//> @attr serverObject.targetXPath     (String : null : IR)
// If set, the SmartClient server will use JXPath to call your server code.  The
// <code>JXPathContext</code> (start point) will be the object arrived at by applying the
// +link{serverObject.lookupStyle,lookupStyle} and related ServerObject properties. The
// intention of this property is to allow easier access to your existing Java objects
// and reduce the need to write SmartClient-specific server code.
//
// @serverDS only
// @visibility external
//<

//> @attr serverObject.crudOnly (boolean : null : IR)
//
// For a ServerObject defined at the +link{DataSource.serverObject,DataSource level}, by
// default we only allow it to intercept standard CRUD operations (ie, ordinary fetches, adds,
// updates and removes).  To allow the ServerObject to intercept other types of operation -
// custom operations, validations, etc - set this property to false.  Note that ServerObjects
// declared at the +link{OperationBinding.serverObject,OperationBinding level} always intercept
// that operation, whatever its type, and this property has no effect.
// <p>
// <b>NOTE:</b> If you are intercepting operations on the server because you wish to inspect
// them before deciding whether to process them with bespoke code or allow them to proceed
// with normal processing, the way to invoke normal processing without causing any interference
// is:<pre>
//    return dsRequest.execute();
// </pre>
//
// @requiresModules SCServer
// @serverDS only
// @visibility external
//<

// OperationBindings
// ---------------------------------------------------------------------------------------

//> @attr DataSource.operationBindings (Array of OperationBinding : null : IR)
// Optional array of OperationBindings, which provide instructions to the DataSource about how each
// DSOperation is to be performed.
// <P>
// When using the SmartClient Server, OperationBindings are specified in your DataSource
// descriptor (.ds.xml file) and control server-side behavior such as what Java object to route
// DSRequest to (+link{operationBinding.serverObject}) or customizations to SQL, JQL and HQL queries
// (+link{operationBinding.customSQL}, +link{operationBinding.customJQL} and +link{operationBinding.customHQL}).
// See the +explorerExample{javaDataIntegration,Java Integration samples}.
// <P>
// For DataSources bound to WSDL-described web services using
// +link{DataSource.serviceNamespace}, OperationBindings are used to bind each DataSource
// +link{operationBinding.operationType,operationType} to an
// +link{operationBinding.wsOperation,operation} of a WSDL-described
// +link{WebService,web service}, so that a DataSource can both fetch and save data to a web
// service.
// <P>
// For example, this code accomplishes part of the binding to the
// +externalLink{http://www.google.com/search?q=sforce+partner+wsdl,SalesForce partner web services}
// <pre>
// isc.DataSource.create({
//    serviceNamespace : "urn:partner.soap.sforce.com",
//    operationBindings : [
//        { operationType:"fetch", wsOperation:"query", recordName: "sObject" },
//        { operationType:"update", wsOperation:"update", recordName: "SaveResult" },
//        { operationType:"add", wsOperation:"create", recordName: "SaveResult" },
//        { operationType:"remove", wsOperation:"delete", recordName: "DeleteResult" }
//    ],
//    ...
// });
// </pre>
// NOTE: additional code is required to handle authentication and other details, see the
// complete code in smartclientSDK/examples/databinding/SalesForce.
// <P>
// For DataSources that contact non-WSDL-described XML or JSON services, OperationBindings can
// be used to separately configure the URL, HTTP method, input and output processing for each
// operationType.  This makes it possible to fetch JSON data from one URL for the "fetch"
// operationType and save to a web service for the "update" operationType, while appearing as a
// single integrated DataSource to a +link{DataBoundComponent} such as an
// +link{ListGrid.canEdit,editable ListGrid}.
// <P>
// If no operationBinding is defined for a given DataSource operation, all of the properties
// which are valid on the operationBinding are checked for on the DataSource itself.
// <P>
// This also means that for a read-only DataSource, that is, a DataSource only capable of fetch
// operations, operationBindings need not be specified, and instead all operationBinding
// properties can be set on the DataSource itself.  An example of using OperationBinding
// properties directly on the DataSource in order to read an RSS feed can be found here:
// <P>
// +standaloneExample{/examples/databinding/rss_databinding.html}
//
// @see class:OperationBinding
// @serverDS allowed
// @visibility xmlBinding
//<

//> @class OperationBinding
// An operationBinding tells a DataSource how to execute one of the basic DS operations: fetch,
// add, update, remove.  See +link{dataSource.operationBindings}.
//
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.operationType (DSOperationType : null : IR)
// Which operationType this operationBinding is for.  This property is only settable on an
// operationBinding, not a DataSource as a whole.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.operationId (String : null : IR)
// Optional operationId if this DataSource supports two or more variants of one of the basic
// DataSource operations, for instance, a "fetch" that uses full text search and a "fetch" that
// accepts per-field search criteria.  See +link{dsRequest.operationId} for usage.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.serverMethod (string : null : IR)
//
// The name of the method to invoke on the +link{ServerObject} for this operationBinding.
// <p>
// <b>NOTE:</b> If you have a +link{DataSource.serverObject,DataSource-level ServerObject} and
// wish to override this operation so that it simply calls a different method on the same
// server object, it is sufficient to specify just this property on the operationBinding:
// there is no need to redefine the serverObject at the operationBinding level.
//
// @requiresModules SCServer
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.methodArguments (string : null : IR)
//
// You can explicitly declare the arguments to be passed to
// +link{operationBinding.serverMethod} using this attribute.  This isn't required - in the
// absence of <code>methodArguments</code>, the DMI implementation will still automatically
// pass a stock set of arguments to your method (see the overview in +link{ServerObject}), but
// specifying arguments gives you the ability to call pre-existing methods without adding
// SmartClient-specific code.
// <p>
// The format for specifying <code>methodArguments</code> is as a comma separated list of VTL
// (Velocity Template Language) expressions.  See the
// +externalLink{http://jakarta.apache.org/velocity/docs/vtl-reference-guide.html, VTL Reference}
// and +externalLink{http://jakarta.apache.org/velocity/docs/user-guide.html, Velocity User Guide}
// for an overview of how to use VTL.
// <p>
// The Velocity context is pre-populated with the following variables - you can pass these
// verbatim as arguments, or call methods on these objects and pass the resulting values:
// <ul>
// <li>dsRequest: instance of the current DSRequest
// <li>request: the current HttpServletRequest
// <li>response: the current HttpServletResponse
// <li>rpcManager: the instance of RPCManager for this request
// <li>dataSource: a DataSource instance for this request
// </ul>
// So, for example, if you had a method signature like the following:
// <p><code>
// public DSResponse fetch(SupplyItem criteria, long startRow, long endRow)
// </code><p>
// You can invoke it by specifying <code>methodArguments</code> as follows:
// <p><code>
// methodArguments="$dsRequest.criteria, $dsRequest.startRow, $dsRequest.endRow"
// </code></p>
// Without <code>methodArguments</code>, there would be no way for you to specify
// <code>startRow/endRow</code> as arguments.  You could, of course, simply declare the method
// to take a <code>DSRequest</code> object and call <code>getStartRow()/getEndRow()</code> in
// the body of the method.
//
// @see ServerObject
//
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.serverObject (ServerObject : null : IR)
//
// Optional ServerObject declaration that specifies the ServerObject configuration for this
// operationBinding.  In the absence of a serverObject specification here, the one specified on
// the DataSource itself is used by default, if present (+link{dataSource.serverObject}).  If
// neither is present, then Direct Method Invocation will not be enabled for this operationBinding.
//
// @see dataSource.serverObject
// @requiresModules SCServer
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.script (String : null : IR)
// Scriptlet to be executed prior to the DataSource operation which is configured by this
// operationBinding.
// <P>
// Scriptlets are used similarly to DMIs configured via +link{operationBinding.serverObject} -
// they can add business logic by modifying the DSRequest before it's executed, modifying the
// default DSResponse, or taking other, unrelated actions.
// <P>
// For example:
// <pre>
//    &lt;operationBindings&gt;
//       &lt;operationBinding operationType="add"&gt;
//           &lt;script language="groovy"&gt;
//              ... Groovy code ...
//           &lt;/script&gt;
//       &lt;/operationBinding&gt;
//    &lt;/operationBindings&gt;
// </pre>
// <P>
// Scriptlets can be written in any language supported by the "JSR 223" standard, including Java
// itself.  See the +link{group:dmiOverview,DMI Script Overview} for rules on how to return data,
// add additional imports, and other settings.
// <P>
// The following variables are available for DMI scriptlets:
// <ul>
// <li><i>dataSource</i>: the current DataSource
// <li><i>dsRequest</i>: the current DSRequest
// <li><i>criteria</i>: shortcut to DSRequest.getCriteria() (a Map)
// <li><i>values</i>: shortcut to DSRequest.getValues() (a Map)
// <li><i>oldValues</i>: shortcut to DSRequest.getOldValues() (a Map)
// <li><i>sqlConnection</i>: <b>SQLDataSource only</b>: the current SQLConnection object.  If
// using +link{dataSource.autoJoinTransactions,automatic transactions} are enabled, this
// SQLConnection is in the context of the current transaction.
// <li><i>beanFactory</i>: the spring BeanFactory (when applicable)
// </ul>
// <P>
// Scriptlets also have access to a set of contextual variables related to the Servlets
// API, as follows:
// <ul>
// <li><i>servletRequest</i>: the current ServletRequest
// <li><i>session</i>: the current HttpSession
// <li><i>rpcManager</i>: the current RPCManager
// <li><i>servletResponse</i>: the current ServletResponse <b>(advanced use only)</b>
// <li><i>servletContext</i>: the current ServletContext<b>(advanced use only)</b>
// </ul>
// As with DMI in general, be aware that if you write scriptlets that depend upon these variables,
// you preclude your DataSource from being used in the widest possible variety of
// circumstances.  For example, adding a scriptlet that relies on the <code>HttpSession</code>
// prevents your DataSource from being used in a command-line process.
//
// @example scriptingUserSpecificData
// @serverDS only
// @visibility external
//<

// Input Handling
// ---------------------------------------------------------------------------------------

//> @attr operationBinding.wsOperation (String : null : IR)
// Name of the web service operation that will be invoked in order to execute this DataSource
// operation.
// <P>
// Valid only for a DataSource that has a WebService (+link{attr:dataSource.serviceNamespace} is
// set).  Otherwise, use +link{attr:operationBinding.dataURL}.
// <P>
// Setting <code>wsOperation</code> means that +link{dsRequest.data} will be serialized as the
// request message for the specified web service operation, with namespacing and soap encoding
// handled automatically.  See +link{DataSource.transformRequest()} for how to customize what
// data is sent to the server.
//
// @group clientDataIntegration
// @visibility xmlBinding
// @example wsdlBinding
//<

//> @attr operationBinding.dataURL              (URL : null : [IR])
// URL to contact to fulfill DSRequests for this operationBinding.
// <P>
// <code>dataURL</code> is typically set as DataSource.dataURL rather than on each individual
// operationBinding.
// <P>
// <code>dataURL</code> can be omitted for a DataSource using a Web Service
// (+link{attr:dataSource.serviceNamespace} is set).
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.dataProtocol            (DSProtocol : "getParams" : [IR])
// Controls the format in which inputs are sent to the dataURL.
// <p>
// When a DataSource operation such as fetchData() is invoked on this DataSource or a component
// bound to this DataSource, the data passed to the operation, if any, will be sent to the
// <code>dataURL</code>.  The <code>dataProtocol</code> property controls the format in
// which the data is sent: SOAP message, HTTP GET or POST of parameters, etc.
// <P>
// The <code>dataProtocol</code> property need not be set for a DataSource with a WebService
// ( +link{attr:dataSource.serviceNamespace} is set), in this case, SOAP messaging is used by
// default.
// <P>
// Developers may completely bypass the SmartClient comm system by setting dataProtocol to
// <code>"clientCustom"</code>. In this case SmartClient will not attempt to send any data
// to the server after calling +link{DataSource.transformRequest()}. Instead the developer is
// expected to implement <code>transformRequest()</code> such that it performs the necessary
// data action outside of SmartClient, and then calls +link{DataSource.processResponse()},
// passing in the +link{DSRequest.requestId} and an appropriate set of DSResponse properties to
// indicate the result of the action.
// <P>
// NOTE: when +link{dataFormat} is "iscServer", <code>dataProtocol</code> is not consulted.
// Instead, SmartClient uses a proprietary wire format to communicate with the SmartClient
// server, and the server-side DSRequest and DSResponse objects should be used to access
// request data and form responses.
//
// @see type:DSProtocol
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<
//dataProtocol:"getParams", // NOTE: dynamic default determined in getServiceInputs()

//> @attr operationBinding.dataFormat (DSDataFormat : "iscServer" : [IR])
// Format for response data for this operation.
// <P>
// Typically set once for the DataSource as a whole via +link{DataSource.dataFormat}.
//
// @group clientDataIntegration
// @visibility xmlBinding
//<

//> @attr operationBinding.dataTransport      (RPCTransport : RPCManager.defaultTransport : [IR])
// Transport to use for this operation.  Defaults to +link{DataSource.dataTransport}, which in
// turn defaults to +link{RPCManager.defaultTransport}.  This would typically only be set to
// enable "scriptInclude" transport for contacting +link{DataSource.dataFormat,JSON} web
// services hosted on servers other than the origin server.
// <p>
// When using the "scriptInclude" transport, be sure to set +link{dataSource.callbackParam} or
// +link{operationBinding.callbackParam} to match the name of the query parameter name expected
// by your JSON service provider.
//
// @see RPCTransport
// @see dataSource.callbackParam
// @group clientDataIntegration
// @visibility xmlBinding
//<

//> @attr operationBinding.useHttpProxy (boolean : null : IR)
// Whether to use the +link{RPCManager.sendProxied,HttpProxy} servlet to send requests
// described by this operationBinding.  If unset, automatically detects whether using the
// HttpProxy is necessary based on the same-origin policy.
// <P>
// Valid only with +link{dataProtocol} settings other than ISCServer.
//
// @group clientDataIntegration
// @visibility external
//<

//> @attr operationBinding.callbackParam      (String : "callback" : [IR])
// Applies only to dataFormat: "json".  Specifies the name of the query parameter that
// tells your JSON service what function to call as part of the response for this operation.
// <P>
// Typically set once for the DataSource as a whole via +link{DataSource.callbackParam}.
//
// @see dataSource.callbackParam
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<


//> @attr operationBinding.requestProperties (DSRequest Properties : null : [IR])
// Additional properties to pass through to the +link{DSRequest} created for this operation.
// Note that these will be cumulative with and will override on a per-property basis any
// properties set via +link{dataSource.requestProperties}.
// <p>
// These properties are applied before +link{dataSource.transformRequest} is called.
//
// @see DSRequest
// @see dataSource.requestProperties
// @group clientDataIntegration
// @group serverDataIntegration
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.preventHTTPCaching (boolean : null : IR)
// Configures +link{dataSource.preventHTTPCaching} on a per-operationType basis.
// @serverDS allowed
// @visibility external
//<

//> @type DSProtocol
// +link{OperationBinding.dataProtocol} affects how the data in the DSRequest (+link{dsRequest.data})
// is sent to the +link{dataSource.dataURL}.  Listed below are the valid values for
// +link{OperationBinding.dataProtocol} and their behavior.
// <P>
// Note that, when using the SmartClient server, data is automatically translated from
// JavaScript to Java according to the rules described +link{rpcRequest.data,here};
// dataProtocol does not apply and is ignored.
// <P>
// If you are integrating with a +link{RestDataSource,REST} server that requires the more
// obscure +link{rpcRequest.httpMethod}s of "PUT", "DELETE" or "HEAD", you can specify these
// httpMethod settings via +link{operationBinding.requestProperties}.  dataProtocol settings
// that mention "GET" or "POST" are compatible with these additional HTTP methods as well.
// Typical +link{dataSource.operationBindings,operationBindings} for a REST server that uses
// "PUT" and "DELETE" are as follows:
// <pre>
//    operationBindings:[
//       {operationType:"fetch", dataProtocol:"getParams"},
//       {operationType:"add", dataProtocol:"postParams"},
//       {operationType:"remove", dataProtocol:"getParams", requestProperties:{httpMethod:"DELETE"}},
//       {operationType:"update", dataProtocol:"postParams", requestProperties:{httpMethod:"PUT"}}
//    ],
// </pre>
//
// @value "getParams"
//   Data is added to the dataURL, with each property in the data becoming an HTTP parameter,
//   eg http://service.com/search?keyword=foo
// @value "postParams"
//   Data is POST'd to the dataURL, with each property becoming an HTTP parameter,
//   exactly as an HTML form would submit them if it had one input field per property in the
//   data.
// @value "postXML"
//   Data is serialized as XML via +link{dataSource.xmlSerialize()} and POST'd as the
//   HTTP request body with contentType "text/xml".
// @value "soap"
//   Data is serialized as XML via +link{dataSource.xmlSerialize()}, wrapped in a SOAP
//   envelope, and POST'd as the HTTP request body with contentType "text/xml".  Generally
//   only used in connection with a +link{group:wsdlBinding,WSDL web service}.
// @value "postMessage"
//   dsRequest.data is assumed to be a String set up by +link{dataSource.transformRequest()}
//   and is POST'd as the HTTP request body.
// @value "clientCustom"
//   This setting entirely bypasses the SmartClient comm system. Instead of the DataSource sending
//   an HTTP request to the server, the developer is expected to implement
//   +link{DataSource.transformRequest()} to perform their own custom data manipulation logic, and
//   then call +link{DataSource.processResponse()} to handle the results of this action.
//
//
// @see operationBinding.dataProtocol
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.defaultParams    (Object : null : [IRA])
// HTTP parameters that should be submitted with every DSRequest.
// <P>
// Useful for authenticated services that require a sessionId with every request.
// <P>
// Can be set for all operations of a given DataSource as DataSource.defaultParams.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.useFlatFields          (boolean : false : [IR])
// Setting <code>useFlatFields</code> on an operationBinding is equivalent to setting
// +link{dsRequest.useFlatFields} on all DataSource requests with the same +link{operationType}
// as this <code>operationBinding</code>.
// <P>
// Typical usage is to combine operationBinding.useFlatFields with
// +link{dataBoundComponent.useFlatFields,searchForm.useFlatFields}, with the +link{SearchForm}
// bound to the +link{webService.getInputDS(),input message} of the web service operation set
// as +link{operationBinding.wsOperation}.  This allows gratuitous nesting to be consistently
// bypassed in both the user presentation and in the actual XML messaging.
// <P>
// Note that <code>useFlatFields</code> is not generally recommended for use with input messages
// where multiple simple type fields exist with the same name, however if used in this way,
// the first field to use a given name wins.  "first" means the first field encountered in a
// depth first search.  "wins" means only the first field will be available in data binding,
// and only the first field will be populated in the generated XML message.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

// Server-side Summaries / Aggregation
// ---------------------------------------------------------------------------------------

//> @groupDef serverSummaries
// In SmartClient, "summarization" refers to applying functions to DataSource fields to obtain
// <i>summary records</i> calculated from the values of multiple DataSource records.
// <p>
// For example, by applying an "average" function to a DataSourceField representing the dollar
// value of an order, you can calculate the average dollar amount of a set of orders.
// <p>
// Client-side calculation of summary records is a
// +link{ListGrid.showGridSummary,listGrid feature}.  Server Summaries is a feature of the
// SmartClient Server allowing similar summaries to be computed server-side, generally by the
// database engine itself.
// <p>
// See also the related feature which allows a single field to receive a value summarized from
// several related records, +link{dataSourceField.includeSummaryFunction}.
// <p>
// <b>The Server Summaries feature is available with Power or better licenses only.</b>  See
// <a href="http://smartclient.com/product">smartclient.com/product</a> for details.
// <p>
// Summarization can be statically configured directly in a .ds.xml file, or can be dynamically
// configured when sending a +link{DSRequest}, either client-side or server-side.  The
// following examples all assume a DataSource with fields like this:
// <pre>
//    &lt;DataSource ID="dsOrders" ...&gt;
//        &lt;fields&gt;
//            &lt;field name="customerName" type="text" /&gt;
//            &lt;field name="orderDate" type="date" /&gt;
//            &lt;field name="deliveryStatus" type="string" /&gt;
//            ...
//        &lt;/fields&gt;
//    &lt;/DataSource&gt;
// </pre>
// <p>
// <h3>Summaries in DataSource .ds.xml files</h3>
// <p>
// You can declare summarization directly on an +link{OperationBinding,operationBinding} in a
// .ds.xml file, using +link{operationBinding.summaryFunctions} and
// +link{operationBinding.groupBy}.  For example:
// <pre>
//    &lt;DataSource ID="dsOrders" ...&gt;
//    ...
//    &lt;operationBindings&gt;
//      &lt;operationBinding operationType="fetch" operationId="lastOrderDateByCustomer"&gt;
//        &lt;summaryFunctions&gt;
//          &lt;orderDate&gt;max&lt;/orderDate&gt;
//        &lt;/summaryFunctions&gt;
//        &lt;groupBy&gt;customerName&lt;/groupBy&gt;
//      &lt;/operationBinding&gt;
//      &lt;/operationBindings&gt;
//    &lt;/DataSource&gt;
// </pre>
// This would return summary records representing the most recent order per customer.
// Represented in JSON, the returned records would look like:
// <pre>
//   { customerName: "JBar Struts", orderDate:"2012/02/05" },
//   { customerName: "KFoo Widgets", orderDate:"2012/03/01" },
//   ...
// </pre>
// This is analogous to the result of a SQL query like:
// <pre>
//    SELECT
//        max(order.orderData)
//        order.customerName
//    FROM
//        order
//    GROUP BY
//        order.customerName
// </pre>
// Note that, as with SQL, the returned records will <i>only</i> include the fields where summary
// functions were applied or which were used for grouping - "deliveryDate" and other fields are
// not included because in general, summary records may represent data from more than one
// record (there may be more than one record with the "max" value, and consider also "sum" or
// "avg" functions), so it's ambiguous which record's values should be returned for
// non-grouped, non-summarized fields.
// <p>
// <h3>Dynamically Requested Summaries</h3>
// <p>
// +link{dsRequest.summaryFunctions} and +link{dsRequest.groupBy} allow you to dynamically
// request a server summary from client-side code.  For example:
// <smartclient>
// <pre>
//    dsOrders.fetchData(null, <i>callback</i>, {
//        summaryFunctions:{"orderDate": "max"}
//        groupBy:["customerName"],
//    });
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
//  DSRequest requestProperties = new DSRequest();
//  requestProperties.setGroupBy("customerName");
//  requestProperties.setSummaryFunctions(new HashMap() {{
//     put("orderDate", SummaryFunctionType.MAX);
//  }});
//  dsOrders.fetchData(null, <i>callback</i>, request);
// </pre>
// </smartgwt>
// By default such requests are allowed, but such requests can be disallowed on a
// per-DataSource or system-wide level if you have concerns - see
// +link{dataSource.allowClientRequestedSummaries}.
// <p>
// You can also dynamically request summaries from server-side code (for example, in a
// +link{DMI} method):
// <pre>
//   DSRequest dsRequest = new DSRequest("dsOrders", "fetch");
//   dsRequest.setSummaryFunctions(new HashMap() {{
//        put("orderDate", SummaryFunctionType.MAX);
//   }});
//   dsRequest.setGroupBy("customerName");
//   DSResponse dsResponse = dsRequest.execute();
// </pre>
// <p>
// <h3>Criteria, Sort &amp; Data Paging</h3>
// <p>
// Criteria and sort directions are supported for queries that involve server summarization.
// <p>
// Criteria apply to record <b>before</b> summaries are applied.  For example, if the "avg"
// function is being applied to a "price" field, criteria like "price &lt; 5" will eliminate
// records where price is less than 5 <i>before</i> the average price is calculated.  This
// means that client-side filtering may not work as expected with summarized results:
// client-side filter criteria are necessarily applied <i>after</i> summary functions have been
// applied, so may not match the server's behavior.  You can set
// +link{resultSet.useClientFiltering} to disable client-side filtering on a grid via
// +link{listGrid.dataProperties}.  Or individual fields can be marked
// +link{listGridField.canFilter,canFilter:false}.
// <p>
// Sort directions may only target fields that are returned by the query (only those fields
// included in <code>groupBy</code> or where a <code>summaryFunction</code> was applied).
// <p>
// Data paging is also supported, however, consider that for aggregated queries, when asked for
// a second page of data, the database is likely to have to repeat all the work of calculating
// aggregated values.  Turning paging off or setting a generous +link{listGrid.dataPageSize} is
// advised.
// <p>
// <h3>SQL Templating</h3>
// <p>
// +link{group:customQuerying,SQL Templating} is also supported with server summaries.
// Clause-by-clause substitution works normally.
// <p>
// <h3>Summarizing without Grouping</h3>
// <p>
// Declaring just &lt;summaryFunctions&gt; without declaring &lt;groupBy&gt; is allowed.  This
// will always give you exactly one summary record in the result, which will represent the
// summary functions as applied to all records that match the criteria in the DSRequest.
// <p>
// <h3>Grouping without Summarizing</h3>
// <p>
// Declaring just &lt;groupBy&gt; without &lt;summaryFunctions&gt; is also allowed.  This gives
// results similar to a SQL "select distinct": one record per distinct set of values for the
// grouped fields.  This kind of result can be used in various ways; one common use case is
// populating a ComboBoxItem with a list of existing values for a field that already appear in
// DataSource records.
//
// @title Server Summaries
// @visibility external
//<


//> @attr operationBinding.summaryFunctions (Object : null : IR)
// A mapping from field names to +link{type:SummaryFunction,summary functions} to be applied
// to each field.
// <p>
// Valid only for an operation of type "fetch".  See the
// +link{group:serverSummaries,Server Summaries overview} for examples of usage.
//
// @group serverSummaries
// @serverDS only
// @see operationBinding.groupBy
// @visibility external
//<

//> @attr operationBinding.groupBy (Array of String : null : IR)
// List of fields to group by when using +link{group:serverSummaries,server-side summarization}.
// <p>
// Valid only for an operation of type "fetch".  See the
// +link{group:serverSummaries,Server Summaries overview} for details and examples of usage.
//
// @group serverSummaries
// @serverDS only
// @see operationBinding.summaryFunctions
// @visibility external
//<

//> @attr dataSourceField.includeSummaryFunction (SummaryFunction : null : R)
// When +link{dataSourceField.includeFrom,field.includeFrom} is specified and multiple records
// exist in the related DataSource per record in the including DataSource,
// <code>includeSummaryFunction</code> indicates which +link{SummaryFunction} is used to
// produce the field value.
// <p>
// <b>This feature is available with Power or better licenses only.</b>
// See <a href="http://smartclient.com/product">smartclient.com/product</a> for details.
// <p>
// For example, give a DataSource "order" and related DataSource "orderItem", the "itemCount"
// field below will show the total number of items in each order.
// <pre>
//  &lt;DataSource ID="order"&gt;
//     &lt;fields&gt;
//        &lt;field name="orderID" type="integer" primaryKey="true" foreignKey="orderItem.orderID" /&gt;
//        &lt;field name="customerName" type="text" /&gt;
//        &lt;field name="orderDate" type="date" /&gt;
//        &lt;field name="itemsCount" includeFrom="orderItem.quantity" includeSummaryFunction="sum" /&gt;
//     &lt;/fields&gt;
//  &lt;/DataSource&gt;
// </pre>
// This is analogous to the following SQL statement:
// <pre>
// SELECT
//      order.orderID,
//      order.customerName,
//      order.orderDate,
//      (select sum(orderItem.quantity)
//          from orderItem
//          where orderItem.orderID = order.orderID) as itemsCount
// FROM
//      order
// </pre>
// Some other common uses:
// <ul>
// <li> using "max" to show the most recent "order" for a "customer"
// <li> using "avg" to show the average order size for a "customer"
// <li> using "concat" to show the names of all "salesReps" involved in an "order" (note:
//      "concat" has limited support - see +link{type:SummaryFunction}).
// </ul>
// <p>
// <b>NOTE</b>: <code>includeSummaryFunction</code> and
// +link{group:serverSummaries,Server Summaries} cannot be used in the same +link{DSRequest}.
// If both configurations are present, Server Summaries settings always take priority.
//
// @group serverSummaries
// @serverDS only
// @see SummaryFunction
// @visibility external
//<

//> @attr dataSource.allowClientRequestedSummaries (boolean : null : IR)
// If a +link{DSRequest} arrives from the client that requests
// +link{group:serverSummaries,server-calculated summaries}, should it be allowed?
// <p>
// Note this setting <b>only</b> affects <code>dsRequests</code> that come from the browser (or
// another client).  This setting has no effect on server summaries declared in .ds.xml files or
// summaries configured in DSRequests created programmatically on the server side, which are
// always allowed.
// <p>
// Default value of null means this DataSource will use the system-wide default, which is set via
// <code>datasources.allowClientRequestedSummaries</code> in
// +link{group:server_properties,server.properties}, and defaults to allowing client-requested
// summaries.
// <p>
// If client-requested summarization is allowed, but the server-side &lt;operationBinding&gt;
// provides specific summarization settings, the client-requested summarization is ignored.
//
// @group serverSummaries
// @serverDS only
// @visibility external
// @see dataSourceField.allowClientRequestedSummaries
//<

//> @attr dataSourceField.allowClientRequestedSummaries (boolean : null : IR)
// Affects whether client dsRequests can request summaries for this field.
// <p>
// Default setting of null means that the DataSource-wide default setting
// +link{dataSource.allowClientRequestedSummaries} is used for this field.
//
// @group serverSummaries
// @serverDS only
// @visibility external
// @see dataSource.allowClientRequestedSummaries
//<

// XML Output handling
// ---------------------------------------------------------------------------------------

//> @attr operationBinding.recordXPath          (XPathExpression : null : [IR])
// For an XML or JSON DataSource, XPath expression used to retrieve the objects that will
// become DataSource records.
// <p>
// For example, an "ItemSearch" web service might return a "Results" structure containing
// metadata along with the set of Items that one might want to display in a grid.  An XPath
// expression like "/Results/Items" could be used to retrieve just the Items, which would then
// become DataSource records.
// <p>
// For a JSON web service, the <code>recordXPath</code> is applied to the returned JSON data
// via +link{XMLTools.selectObjects()}.  Only limited XPath syntax is allowed; see
// +link{XMLTools.selectObjects(),selectObjects()} for details.
// <P>
// For processing XML results, see +link{attr:operationBinding.xmlNamespaces} for information
// on the namespaces that are available in this XPath expression.  If you are contacting a WSDL
// web service, note that +link{operationBinding.recordName} is an alternative way to specify
// which records should be selected by their tagName or type, and this is usually simpler.
// <P>
// To learn about XPath, try the following search:
// <a href="http://www.google.com/search?q=xpath+tutorial" target="_blank"
// >http://www.google.com/search?q=xpath+tutorial</a>
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.recordName           (String : null : [IR])
// For an XML DataSource, tagName of the elements to be used as records.
// <p>
// This is a simple alternative to +link{attr:recordXPath} when the elements to be used as
// records all share a tagName.
// <p>
// When a DataSource has a WebService, <code>recordName</code> can also be set to the name
// of any <code>complexType</code> declared within the WebService's WSDL file.
//
// @group clientDataIntegration
// @visibility xmlBinding
// @serverDS allowed
// @example wsdlBinding
//<

//> @attr operationBinding.spoofResponses (boolean : false : IR)
// For a DataSource contacting a +link{dataSource.serviceNamespace,WSDL web service}, setting
// this flag means the DataSource doesn't actually attempt to contact the server but generates
// a sample response instead, based on the XML Schema of the response message embedded in the
// WSDL.
// <P>
// The spoofed response will include all complexType elements and will fill in appropriate
// values by type for all simpleType elements, although the spoofed data will not conform to
// all xs:restriction declarations (eg xs:pattern).
// <P>
// Note that if your WSDL does not fully describe the response format (some WSDL services just
// have a placeholder &lt;xs:any&gt; element), SmartClient can only produce a partial
// response.  To use a hand-generated sample response, just save an XML file to disk and use
// the +link{operationBinding.dataURL} setting to point to it.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

//> @attr operationBinding.xmlNamespaces              (Object : null : [IR])
// Optional object declaring namespace prefixes for use in +link{attr:recordXPath} and
// +link{attr:DataSourceField.valueXPath} XPath expressions.
// <P>
// <code>xmlNamespaces</code> should be specified as a mapping from namespace prefix to
// namespace URI, for example:
// <pre>
//    xmlNamespaces : {
//        az : "http://webservices.amazon.com/AWSECommerceService/2005-03-23"
//    }
// </pre>
// By default, all namespaces declared on the document element (outermost element of the
// response) are made available with the prefix used in the document itself.
// <P>
// Then, for non-WSDL-described XML results, if there is a default namespace on the document
// element, it is made available with the special prefix "default".
// <P>
// For results of WSDL-described operations, the prefix "service" means the service namespace,
// that is, the "targetNamespace" on the &lt;definitions&gt; element from the WSDL file.  The prefix
// "schema" means the namespace of the outermost element in the output message for the current
// operation.  "default" will be the schema namespace if there is one, otherwise the service
// namespace.
// <P>
// For basic information on XML Namespaces and their use in XPath, try the following search:
// <a href="http://www.google.com/search?q=XPath+xml+namespaces" target="_blank"
// >http://www.google.com/search?q=XPath+xml+namespaces</a>
//
// @group clientDataIntegration
// @visibility xmlBinding
// @serverDS allowed
// @example xmlYahooWebServices
//<

//> @attr operationBinding.responseDataSchema           (DataSource : null : [IRA])
// Optional schema describing how to extract DataSource records from the XML elements selected.
// <P>
// Once a set of XML elements have been selected via <code>recordXPath</code> or
// <code>recordName</code>, those elements are normally transformed to JavaScript objects
// using the <code>fields</code> of the DataSource that owns the operationBinding.  A
// <code>responseDataSchema</code> can be specified instead if the XML differs in some way between
// different DataSource operations, such that different values for
// +link{attr:DataSourceField.valueXPath,field.valueXPath} may be necessary to extract the same
// DataSource record from slightly different XML structures.
//
// @group clientDataIntegration
// @serverDS allowed
// @visibility xmlBinding
//<

// Export
// ---------------------------------------------------------------------------------------

//> @attr operationBinding.exportResults (boolean : false : IR)
// When set, causes the results of the DataSource Operation to be exported to a file, whose
// name and format are indicated by +link{operationBinding.exportFilename} and
// +link{operationBinding.exportAs} respectively.  When no exportFilename is provided, the
// default is <i>Results</i> and the default value of exportAs is <i>csv</i>.  Once the Operation
// completes, +link{dsRequest.exportDisplay} specifies whether the
// exported data will be downloaded to the file-system or displayed in a new window.   The
// default value of exportDisplay is "download" which displays the Save As dialog.  See
// +link{ExportDisplay} for more information.
// <P>
// The export field-list can also be configured, see +link{dsRequest.exportFields}.
// <P>
// You can also configure the style of line-breaks to use when generating the output.  See
// +link{LineBreakStyle} for more information.
// <P>
// As well as setting this and other properties on the +link{OperationBinding}, Exports can be
// initiated in two other ways.  You can set properties on the dsRequest by passing
// <i>requestProperties</i> into +link{dataSource.exportData()}.  Note that this method does
// not support exporting to JSON format (see
// <a href="http://forums.smartclient.com/showthread.php?t=235">this post</a> for more detail).
// Additionally, custom server code may set export-related properties on the
// +link{DSResponse}.
// <P>
// <b>Format Examples</b>
// <P>
// XML format
// <pre>
//     &lt;List&gt;
//         &lt;Object&gt;
//             &lt;id&gt;10101&lt;/id&gt;
//             &lt;displayName&gt;Record 10101&lt;/displayName&gt;
//         &lt;/Object&gt;
//    &lt;/List&gt;
// </pre>
// JSON Format
// <pre>
//     [
//         { id: 10101, displayName: "Record 10101" }
//     ]
// </pre>
// CSV Format
// <pre>
//     id,displayName
//     10101,"Record 10101"
// </pre>
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.exportAs (ExportFormat : "csv" : IR)
// The format in which the data should be exported.  Default is "csv".
// See +link{ExportFormat} for more information.
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.exportFilename (String : null : IR)
// The name of the file to save the exported data into.
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.lineBreakStyle (String : null : IR)
// The style of line-breaks to use in the exported output.  See +link{LineBreakStyle} for
// more information.
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.exportFields (Array : null : IR)
// The list of field-names to export.  If provided, the field-list in the exported output is
// limited and sorted as per the list.
// <P>
// If exportFields is not provided, the exported output includes all visible fields
// from the DataSource (field.hidden=false), sorted in the order they're defined.
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.autoJoinTransactions (boolean : null : IR)
// If true, causes requests against this operation to automatically start or join a transaction.
// if false, causes requests against this operation to be committed individually.  If null,
// falls back to +link{DataSource.autoJoinTransactions}.
// <P>
// See +link{DataSource.autoJoinTransactions} for further details of SmartClient's automatic
// transaction control.
//
// @serverDS only
// @visibility transactions
//<

// Velocity template variables
// ---------------------------------------------------------------------------------------
//> @groupDef velocitySupport
// The SmartClient Server provides a number of standard context variables for use in the
// Velocity templates you write to implement +link{group:customQuerying,custom queries},
// +link{dsRequestModifier.value,transaction chaining},
// +link{operationBinding.requires,dynamic security checking} and
// +link{object:Mail,templated mail messages}.  These are:
// <ul>
// <li><b>$currentDate</b>. The current date/time with millisecond precision</li>
// <li><b>$transactionDate</b>. The date/time that this transaction started, with millisecond
// precision.  If you are not using +link{RPCManager.startQueue,queuing}, this value will be
// identical to <b>$currentDate</b></li>
// <li><b>$servletRequest</b>. The associated <code>HttpServletRequest</code></li>
// <li><b>$dsRequest</b>. The associated +link{class:DSRequest} (though of course this is a
// server-side <code>DSRequest</code> object, so please also see the server-side Javadocs)</li>
// <li><b>$primaryDSRequest</b>. Only present on cache-sync operations, this is the original
// update <code>DSRequest</code> that caused the cache-sync request to be created</li>
// <li><b>$session</b>. The associated <code>HttpSession</code></li>
// <li><b>$httpParameters</b>. This variable gives you access to the parameters Map of the
// associated <code>HttpServletRequest</code>; it is an alternate form of
// <code>$servletRequest.getParameter</code></li>
// <li><b>$requestAttributes</b>. This variable gives you access to the attributes Map of the
// associated <code>HttpServletRequest</code>; it is an alternate form of
// <code>$servletRequest.getAttribute</code></li>
// <li><b>$sessionAttributes</b>. This variable gives you access to the attributes Map of the
// associated <code>HttpSession</code>; it is an alternate form of
// <code>$session.getAttribute</code></li>
// <li><b>$dataSources</b>. This variable gives you access to SmartClient +link{class:DataSource}s.
// You access a dataSource by suffixing its name to the <code>$dataSources</code> designation.
// For example, <code>$dataSources.supplyItem</code> refers to the DataSource object called
// "supplyItem".  You can use this approach to execute any valid DataSource method. One especially
// useful method in this context is <code>hasRecord(fieldName, value)</code> - see the
// server-side Javadocs for more details.
// <li><b>$util</b> - A <code>DataTools</code> object, giving you access to all of that
//     class's useful helper functions</li>
// <li><b>$log</b> - A <code>Logger</code> instance in category "velocityTemplate"</li>
// <li><b>$rpc</b> - the current <code>RPCManager</code></li>
// <li><b>$rpcManager</b> - the current <code>RPCManager</code> (synonym to $rpc)</li>
// <li><b>$storedRecord</b> - The record as it currently exists in storage. Fetched only if accessed.
//     Fetched only once per validation run. If record does not exist in storage (add operation) velocity
//     engine will complain about missing properties. To avoid it - use special property <b>recordExists</b>
//     to test if specified record is found in storage. For example (new value can only be greater then existing):
//     <pre>
//         #if ($storedRecord.recordExists())
//             $value &gt; $storedRecord.valInt
//         #else
//             true
//         #end</pre></li>
// <li><b>$editedRecord</b> - The stored record with submitted record overlaid as changes. If record does not
//     exists $editedRecord will contain only properties submitted in request. This variable is recalculated
//     for every validator, picking up any changes caused by setting Validator.resultingValue.
//     Validation occurs in the same order that fields are defined in the DataSource.
//     Using $editedRecord, it is invalid to assume that validation for other fields has completed therefore
//     it is not guaranteed that properties will contain correct (validated) values.
//     It can be wrong type (String instead of Integer) as well.</li>
// </ul>
// All of these variables (other than the two dates) represent objects that can contain other
// objects (attributes or parameters or object properties).  The variables based on the Servlet
// API (session, sessionAttributes, httpParameters, servletRequest and requestAttributes) all
// implement the <code>Map</code> interface, so you can use the Velocity "property" shorthand
// notation to access them.  The following usage examples show five equivalent ways to return
// the value of the session attribute named "foo":
// <pre>
//    $session.foo
//    $session.get("foo")
//    $session.getAttribute("foo")
//    $sessionAttributes.foo
//    $sessionAttributes.get("foo")
// </pre>
// In the case of <code>$servletRequest</code>, the shorthand approach accesses the attributes
// - you need to use either <code>$httpParameters</code> or <code>$servletRequest.getParameter</code>
// to access parameters. These examples all return the value of the HTTP parameter named "bar":
// <pre>
//    $httpParameters.bar
//    $httpParameters.get("bar")
//    $servletRequest.getParameter("bar")
// </pre>
// When you use these Velocity variables in a +link{operationBinding.customSQL,customSQL}
// clause or SQL snippet such as a +link{operationBinding.whereClause,whereClause}, all of
// these template variables return values that have been correctly quoted and escaped according
//  to the syntax of the underlying database.  We do this because "raw" values are vulnerable to
// <a href="http://en.wikipedia.org/wiki/SQL_injection">SQL injection attacks</a>.
// If you need access to the raw value of a variable in a SQL template, you can use the
// <b>$rawValue</b> qualifier in front of any of the template variables, like this:<br><br><code>
// &nbsp;&nbsp;$rawValue.session.foo</code>
// <p>
// This also works for the <b>$criteria</b> and <b>$values</b> context variables (see
// +link{group:customQuerying} for details of these variables).  So:<br><br><code>
// &nbsp;&nbsp;$rawValue.criteria.customerName</code>
// </p>
// Note that <code>$rawValue</code> is only available in SQL templates.  It is not needed in
// other contexts, such as Transaction Chaining, because the value is not escaped and
// quoted in these contexts.
// <p>
// <b>Warning</b>:  Whenever you access a template variable for use in a SQL statement, bear
// in mind that it is <b>dangerous</b> to use <code>$rawValue</code>.  There are some cases
// where using the raw value is necessary, but even so, all such cases are likely to be vulnerable
// to injection attacks.  Generally, the presence of <code>$rawValue</code> in a SQL template
// should be viewed as a red flag.
// <p>
// Finally, some example usages of these values.
// These +link{operationBinding.values,values} clauses set "price" to a value extracted from the
// session, and "lastUpdated" to the date/time that this transaction started:<br><br><code>
// &nbsp;&nbsp;&lt;values fieldName="price" value="$session.somePrice" /&gt;<br>
// &nbsp;&nbsp;&lt;values fieldName="lastUpdated" value="$transactionDate" /&gt;
// </code><p>
// This whereClause selects some users based on various values passed in the criteria and
// as HTTP parameters:<br><br><code>
// &nbsp;&nbsp;&lt;whereClause&gt;department = $httpParameters.userDept AND dob &gt;= $criteria.dateOfBirth&lt;/whereClause&gt;
// </code><p>
// This whereClause selects some users based on various values obtained from the
// servletRequest's attributes, using a number of equivalent techniques for accessing the
// attributes:<pre>
//   &lt;whereClause&gt;
//         department = $servletRequest.dept
//     AND startDate &gt;= $requestAttributes.dateOfBirth
//     AND salary &lt; $servletRequest.getAttribute("userSalary")
//   &lt;/whereClause&gt;
// </pre><p>
//
// @title Velocity context variables
// @visibility chaining
//<


// Custom Querying
// ---------------------------------------------------------------------------------------

//> @type DefaultQueryClause
// The Velocity variable names of the "pieces" of SQL that SmartClient generates to form a
// complete fetch or update query.  You can use these variables in you own custom queries
// and query clause overrides to build on the SmartClient functionality.  See
// +link{group:customQuerying} for a full discussion.
//
// @value "$defaultSelectClause"  The column names to select, for a fetch operation only
// @value "$defaultTableClause"   The table name(s) to select from or update
// @value "$defaultWhereClause"   The "where" condition, which will be derived from supplied
//                                criteria or a primary key value, depending on the type of
//                                operation
// @value "$defaultValuesClause"  The column names to update and the update values, for an
//                                update or add operation
// @value "$defaultOrderClause"   The column names to sort by, for a fetch operation only
//
// @group customQuerying
// @serverDS allowed
// @visibility customSQL
//<

//> @groupDef customQuerying
// <b>This feature, called "SQL Templating", is available with Power or better licenses
// only.</b> See <a href="http://smartclient.com/product">smartclient.com/product</a> for details.
// <p>
// The SmartClient server provides a number of ways to let you customize the SQL, JPA or Hibernate
// query it generates to fetch data from or update your database.  You can provide full
// custom queries in either +link{OperationBinding.customSQL,SQL}, +link{OperationBinding.customJQL,JQL} or
// +link{OperationBinding.customHQL,HQL}, or you can replace individual parts of the query
// (+link{OperationBinding.whereClause,the WHERE clause}, for example) while letting
// SmartClient generate the rest.
// <P>
// Full custom queries specified via &lt;customSQL&gt; provide complete flexibility, but
// they cannot be used for automatic data paging; if you use a full custom query, all data
// returned by the query will be delivered to the client, which may be inefficient.
// To retain automatic data paging, implement your customizations by replacing just specific
// clauses of the query, via +link{operationBinding.whereClause,&lt;whereClause&gt;},
// +link{operationBinding.selectClause,&lt;selectClause&gt;}, and the other clause-by-clause
// replacement features.
// <p>
// Query customization is done per +link{OperationBinding}, so you can create multiple
// customized queries per DataSource or even for the same
// +link{OperationBinding.operationType,operation type}.
//
// <h4>Using criteria and values</h4>
// Whether using full custom queries or individual clauses, your code has access to the
// criteria values submitted with the operation; if this is an "add" or "update" operation,
// it also has access to the new field values sent from the client.
// <p>
// Fields are accessed in your SQL, JQL or HQL code using the Velocity template language.  You
// can refer to container variables <b>$criteria</b> and <b>$values</b> in your queries or
// clause snippets, and SmartClient will insert the appropriate values.  A simple
// +link{OperationBinding.whereClause,whereClause} example:
// <p>
// <pre><code>
//  &lt;operationBinding operationType="fetch"&gt;
//     &lt;whereClause&gt;
//         continent = $criteria.continent AND population &gt; $criteria.minPop
//     &lt;/whereClause&gt;
//  &lt;/operationBinding&gt;
// </code></pre>
// In addition to the $criteria and $values Velocity template variables described above, we
// also provide a number of template variables containing generally-useful values.  Please see
// +link{group:velocitySupport} for details.
//
// <h4>Using the default clauses</h4>
// You also have access to the +link{type:DefaultQueryClause,default subclauses} generated by
// SmartClient.  You can use these in full custom queries to allow a certain part of the query
// code to be generated:
// <p>
// <pre>
// &lt;customSQL&gt;
//    SELECT foo, bar FROM $defaultTableClause
//        WHERE baz &gt; $criteria.baz
// &lt;/customSQL&gt;
// </pre>
// <p>
// You can also use them within individual clauses in order to customize a clause without
// losing default SQL generation:
// <pre>
// &lt;whereClause&gt;
//    ($defaultWhereClause) AND foo &gt; 5
// &lt;/whereClause&gt;
// </pre>
//
// <h4>Mixing SQL Templating and custom Java Logic</h4>
// You can use both SQL Templating and custom Java logic added via +link{DMI} in the same
// operationBinding.  Your DMI method is called before SQL is generated, and the SQL template
// will be evalauted and the actual SQL operation performed only when you call
// dsRequest.execute().
// <P>
// This allows you to modify the criteria or values on the DSRequest, which will change the
// values retrieved by $criteria and $values when the SQL Template is evaluated.  You can also
// add entirely new information to the Velocity context used to evaluate the template, via
// the server-side API DSRequest.addToTemplateContext().
//
// <h4>Excluding fields from SQL Generation</h4>
// In some cases you pass a value to the server which is intended to be used by custom Java
// logic and should not cause SQL to be generated.  To prevent all SQL from being generated for
// a particular field, set +link{dataSourceField.customSQL,customSQL="true"} on that field.
// <P>
// Any field for which SQL will ever be generated must be declared in a DataSource.  It's
// common to have a field which is only used in one or two operationBindings - in this case,
// set customSQL="true" on the field, and use +link{operationBinding.customFields} to cause
// specific operationBindings to generate SQL for the field, while all others ignore it.
// <P>
// In other cases you want to hand-write SQL for a particular field for a specific
// operationBinding.  You can set +link{operationBinding.excludeCriteriaFields} to
// exclude fields from SQL generation for the whereClause of a specific operationBinding.
//
// <h4>Field-level SQL Customization</h4>
// An individual field can configured with custom expressions to be used in different SQL
// statements for all operationBindings - see +link{dataSourceField.customSQL} for an overview.
//
// <h4>Using AdvancedCriteria</h4>
// The above examples involving $criteria assume that the submitted criteria are simple
// Criteria and not +link{AdvancedCriteria}, a more sophisticated criteria format in which
// different search operators can be specified per field and criteria can be nested.
// <P>
// The special variable $advancedCriteria provides simplified access to the AdvancedCriteria
// structure: $advancedCriteria.<i>fieldName</i> will return the criteria value specified for a
// given fieldName, regardless of where it's present in the AdvancedCriteria.
// <P>
// This makes it straightforward to add an additional criteria value to AdvancedCriteria that
// you want to use only in the SQL template:
// <ul>
// <li> make a simple Criteria object representing the fieldName and value name you need to have
// available in the SQL template
// <li> use +link{DataSource.combineCriteria()} to add your additional criteria to an existing
// AdvancedCriteria, wherever this is convenient
// <li> list the fieldName in +link{operationBinding.customCriteriaFields} to prevent the
// default SQL for this field from being generated
// <li> use $advancedCriteria in your customized SQL to access the value
// </ul>
// Java API dsRequest.getCriteriaValue() is equivalent to accessing $advancedCriteria in a SQL
// Template.  Also note, if a given fieldName appears more than once in AdvancedCriteria,
// $advancedCriteria.<i>fieldName</i> will return the value for the first +link{Criterion} that
// uses the fieldName, as found by depth-first search.
// <p>
// NOTE: $advancedCriteria falls back to simple criteria values if the current criteria object
// is not an <code>AdvancedCriteria</code>.  This means that you can safely use $advancedCriteria
// in circumstances where you cannot predict in advance whether your server code will be handed
// a simple criteria or an AdvancedCriteria.
//
// <h4>Stored procedures</h4>
// It is possible to include templated calls to SQL stored procedures in a
// +link{OperationBinding.customSQL,customSQL} clause, for the ultimate in flexibility.  For
// example, the deletion of an order might require a number of actions: deletion of the order
// record itself, messages sent to other systems (data warehousing, maybe, or a central accounts
// system running on a mainframe), an event log written, and so on.  You could write a stored
// procedure to do all this, and then invoke it with a customSQL clause:
// <pre>
//    &lt;operationBinding operationType="remove"&gt;
//        &lt;customSQL&gt;call deleteOrder($criteria.orderNo)&lt;/customSQL&gt;
//    &lt;/operationBinding&gt;
// </pre>
// When calling stored procedures this way, be sure that the &lt;customSQL&gt; operates like a
// normal SQL statement, so that it can be called via normal JDBC calls.  For
// operationType="fetch", the JDBC API PreparedStatement.executeQuery() is called and expects a
// ResultSet returned.  For "update", "add" and "remove" operationTypes,
// PreparedStatement.executeUpdate() is called and expects an integer return value (number of
// rows affected).  If your stored procedure uses in/out parameters, returns something other
// than a ResultSet or affected row count, or in some other way is incompatble with the
// standard JDBC methods described above, you may need to add code to adjust the return values
// to what JDBC expects.  You can do this by either putting code into the &lt;customSQL&gt;
// block directly, or by adding a second stored procedure that transforms the outputs of the
// first procedure.
//
// <h4>Velocity Template Language conditional logic</h4>
// When writing a customized SQL clause for an operation, it is commonly desirable to be
// able to include conditional logic - for example only modifying a where clause if a
// certain criteria value is present. Velocity template language conditional statements
// can be embedded directly into your template code to achieve this. For example the following
// <code>whereClause</code> would produce different output depending on whether the
// request criteria included a value for the field <code><i>someField</i></code>:<p>
// <code>&lt;whereClause&gt;$defaultWhereClause #if ($criteria.someField) AND someDatabaseField = $criteria.someField #end&lt/whereClause&gt;</code>
// <p>
// If <code><i>criteria.someField</i></code> was not present in the request, the generated
// SQL statement would simply use the default where clause -- otherwise
// <code>AND someDatabaseField = <i>[some value]</i></code> would be appended to it (where
// <code><i>[some value]</i></code> was picked up from the value of <code>someField</code> on
// the request criteria object).
//
// <h4>Custom queries are safe</h4>
// Custom queries are protected from <a href=http://en.wikipedia.org/wiki/SQL_injection>
// SQL injection attacks</a>, because anything coming from the client is quoted and escaped
// in accordance with the syntax of the underlying database before use (though see the warning
// about using <code>$rawValue</code> in the article on +link{velocitySupport}).
// So, in a typical SQL injection attack an attacker might enter his User ID as <br>
// &nbsp;&nbsp;<code>123' OR '1' = '1</code><p>
// in the hope that this will generate a query
// with a where clause like this<br>
//  &nbsp;&nbsp;<code>WHERE userID = '123' OR '1' = '1'</code><p>
// which would of course return every row.  With SmartClient custom queries, this does not happen;
// the client-provided string is escaped, and the resultant clause would look like this: <br>
// &nbsp;&nbsp;<code>WHERE userID = '123'' OR ''1'' = ''1'</code><p>
// This clause only returns those records where the userID column contains the literal value that
// the user typed: <br>
// &nbsp;&nbsp;<code>123' OR '1' = '1</code>
// <p>
// Further, custom queries can be protected from buggy or ad-hoc client requests because the
// query is specified on the server.  For example you could add a custom where clause, as shown
// in the above section on default clauses, to ensure that certain records are never seen by
// the client.  For instance: <p>
// <code>&lt;whereClause&gt;($defaultWhereClause) AND confidential = '0'&lt;/whereClause&gt;</code>.
// <p>
// <h4>Column case-sensitivity issues</h4>
// Different database products have different rules concerning case-sensitivity in column
// names.  Consider the following query:
// <br><br><code>&nbsp;&nbsp;SELECT orderNumber FROM Order</code>
// <ul>
// <li>MySQL and Microsoft SQL Server are not case-sensitive with regard to column names, so
// this query will work whether the column is called "orderNumber" or "ORDERNUMBER" or any
// other variation.</li>
// <li>Oracle, HSQLDB and DB2 default to upper-case column names.  Therefore, this query will
// fail if the column is actually called "orderNumber"; it will only work if the underlying
// column name is "ORDERNUMBER"</li>
// <li>PostgreSQL and Informix default to lower-case column names, so this query will fail
// unless the underlying column name is actually "ordernumber"</li>
// </ul>
// Note that these differences only apply in a practical sense if the underlying database
// table was created with quoted column names.  If you create your tables without quoting
// column names, the database creates the columns using its own preferred defaults, which
// is what it will also use when it encounters an unquoted column name in a query.  Behind
// the scenes, the differences are still there - your column will be called "ORDERNUMBER"
// on Oracle and "ordernumber" on PostgreSQL - but that wouldn't be apparent unless you went
// looking for it: the example query would work unchanged with both products, and you would
// be able to use whatever mixture of case you like in your DataSource field names
// (SmartClient will map DataSource field "orderNumber" to Oracle column "ORDERNUMBER"
// transparently).  <b>This is the recommended approach.</b>
// <p>
// If you can't, or don't want to, accept the database default - if you are working with an
// existing schema, for example - then you will need to quote column names in your queries.
// Unfortunately, the way you do this also differs by database product, so quoting a column
// name correctly in one database's syntax may mean that the query cannot be ported to a
// different database without change.
// <p>
// To help with this case, we provide two extra container variables that you can use.
// <b>$fields</b> contains the names of all the fields in your DataSource, but quoted in
// accordance with the column-quoting rules of the target database.  <b>$qfields</b> also
// contains a list of field names, but in this case each one is qualified with its table
// name.<p>
// As an example of how to use <b>$fields</b> and <b>$qfields</b>, consider a DataSource with
// a field called "itemID", bound to a column also called "itemID", and a tableName property
// of "orderItem".  Here are three ways to write a +link{OperationBinding.selectClause,selectClause}
// for a custom SQL query that returns that field:<ul>
// <li><code>orderItem."itemID"</code>
// <li><code>orderItem.$fields.itemID</code>
// <li><code>$qfields.itemID</code>
// </ul>
// The first of these is not portable.  It will work fine in HSQL and Oracle, but will fail
// with a syntax error in MySQL because you quote a field name with backticks in MySQL, not
// quote marks.
// <p>
// The usages via <b>$fields</b> and <b>$qfields</b> <em>are</em> portable.  The second line,
// when targeting Oracle, will be translated to <code>orderItem."itemID"</code>; when targeting
// MySQL, it will be translated to <code>orderItem.itemID</code>, or <code>orderItem.`itemID`</code>
// if column quoting is enabled for that database (it generally isn't required, since MySQL
// preserves case by default).
//
// @treeLocation Client Reference/Data Binding/DataSource
// @title Custom Querying Overview
// @visibility customSQL
//<



//> @attr operationBinding.selectClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// SELECT clause to use when constructing the SQL query to perform this operation.  The
// property should be a comma-separated list of column names and/or expressions, and you
// can refer to any scalar function supported by the underlying database.  The server will
// insert the text of this property immediately after the "SELECT" token.
// <p>
// Note that if you also specify a +link{groupClause,groupClause}, you can use aggregate
// functions such as SUM and COUNT in the selectClause.
// <p>
// This property is only applicable to operationBindings of
// +link{OperationBinding.operationType,operationType} "fetch".
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.tableClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// table clause to use when constructing the SQL query to perform this operation.  The
// property should be a comma-separated list of tables and views, and you can use any
// special language constructs supported by the underlying database.  The server will
// insert the text of this property immediately after the "FROM" token.
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.whereClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// WHERE clause to use when constructing the SQL query to perform this operation.  The
// property should be a valid expression in the syntax of the underlying database.  The
// server will insert the text of this property immediately after the "WHERE" token.
// <p>
// You may find the SmartClient-provided <b>$criteria</b> variable of particular use with
// this property.
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.groupClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// GROUP BY clause to use when constructing the SQL query to perform this operation.  The
// property should be a comma-separated list of column names and/or expressions, forming
// a valid GROUP BY clause in the syntax of the underlying database.  The server will
// insert the text of this property immediately after the "GROUP BY" token.
// <p>
// Note that specifying this property enables you to use aggregate functions (such as
// COUNT and SUM) in your +link{selectClause,selectClause}. Also note that care is required
// when using groupClause to ensure that the selectClause contains the fields you are grouping
// by.  Failure to do this correctly will result in a runtime SQL error.
// <p>
// This property is only applicable to operationBindings of
// +link{OperationBinding.operationType,operationType} "fetch".
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @see OperationBinding.selectClause
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.orderClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// ORDER BY clause to use when constructing the SQL query to perform this operation.  The
// property should be a comma-separated list of column names and/or expressions, forming
// a valid ORDER BY clause in the syntax of the underlying database.  The server will
// insert the text of this property immediately after the "ORDER BY" token.
// <p>
// This property is only applicable to operationBindings of
// +link{OperationBinding.operationType,operationType} "fetch".
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.valuesClause (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql", this
// property can be specified on an operationBinding to provide the server with a bespoke
// set of values to add or update,  for use when constructing the SQL query to perform this
// operation.  The property should be one of the following, depending on the
// +link{operationType,operationType}:
// <p>
// For "add" operations, the syntax that would be valid for an INSERT INTO query: a
// comma-separated list of column names enclosed in parentheses, followed by a comma-separated
// list of new values, enclosed in parentheses and preceded by the token "VALUES".  For example:
// <p>
// <code>&lt;valuesClause&gt;(name, age) VALUES("Jane Doe", 48)&lt;/valuesClause&gt;</code>
// <p>
// For "update" operations, the syntax that would be valid for an UPDATE query: a
// comma-separated list of expressions equating a column name to its new value.  For example:
// <p>
// <code>&lt;valuesClause&gt;name="Jane Doe", age=48&lt;/valuesClause&gt;</code>
// <p>
// You may find the SmartClient-provided <b>$values</b> variable of particular use with
// this property.
// <p>
// See the documentation for +link{customSQL} for usage examples
//
// @group customQuerying
// @see OperationBinding.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.customSQL (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "sql" or "hibernate", this
// property can be specified on an operationBinding to indicate that the server should run
// user-specified SQL, rather than the query it would normally generate to satisfy a
// dataSource operation.  This property allows you to provide a fully-customized query; an
// alternative is to provide custom "pieces" to the query generator, via properties such as
// +link{whereClause,whereClause} and +link{valuesClause,valuesClause}.  See the
// +link{group:customQuerying} for more details.
// <p>
// For a dataSource of type "sql", the SmartClient server generates a number of useful
// +link{type:DefaultQueryClause, query "pieces"},
// and makes them available to your custom SQL code via the Velocity templating
// language (note that this is not available for "hibernate" dataSources).
// <p>
// We also make the template variables <b>$criteria</b> and <b>$values</b> available, to give
// you direct access to the supplied criteria, and to the new field values for update and add
// operations.  These variables are available to both "sql" and "hibernate" dataSources.
// <p>
// Note that you should use this feature with care. In particular, writing customSQL code
// that makes use of a particular database engine's features or syntax will make your
// application less portable.
// <p>
// See +link{group:customQuerying} for an overview of writing custom queries and clauses.
// <p>
// <h3>Examples</h3>
// An example using the SmartClient-supplied query pieces.  This custom query will give
// exactly the same result as the SmartClient-generated query:
// <p>
// <code>
// &lt;operationBinding operationId="customFetch" operationType="fetch"&gt;<br>
// &nbsp;&nbsp;&lt;customSQL&gt;<br>
// &nbsp;&nbsp;&nbsp;&nbsp;SELECT $defaultSelectClause FROM $defaultTableClause
// WHERE $defaultWhereClause ORDER BY $defaultOrderClause<br>
// &nbsp;&nbsp;&lt;/customSQL&gt;<br>
// &lt;/operationBinding&gt;<br>
// </code>
// <p>
// An example using the SmartClient-supplied <b>$criteria</b> template variable:
// <p>
// <code>
// &lt;operationBinding operationId="customFetch" operationType="fetch"&gt;<br>
// &nbsp;&nbsp;&lt;customSQL&gt;<br>
// &nbsp;&nbsp;&nbsp;&nbsp;SELECT foo, bar, baz FROM MyTable WHERE bar &gt; $criteria.someValue<br>
// &nbsp;&nbsp;&lt;/customSQL&gt;<br>
// &lt;/operationBinding&gt;<br>
// </code>
// <p>
// An update example:
// <p>
// <code>
// &lt;operationBinding operationId="myUpdateOp" operationType="update"&gt;<br>
// &nbsp;&nbsp;&lt;customSQL&gt;<br>
// &nbsp;&nbsp;&nbsp;&nbsp;UPDATE $defaultTableClause SET $defaultValuesClause WHERE bar
// &lt;= $criteria.someValue<br>
// &nbsp;&nbsp;&lt;/customSQL&gt;<br>
// &lt;/operationBinding&gt;<br>
// </code>
//
// @group customQuerying
// @see OperationBinding.customHQL
// @see DataSourceField.customSQL
// @serverDS only
// @visibility customSQL
//<



//> @attr operationBinding.customHQL (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "hibernate", this
// property can be specified on an operationBinding to indicate that the server should run
// user-specified HQL (Hibernate Query Language), rather than the Hibernate criteria query or
// <code>saveOrUpdate</code> call it would normally generate to satisfy a dataSource operation
// via Hibernate.
// <p>
// Note that inserting new records via HQL is often impractical, due to intentional restrictions
// in the language (it is only possible to perform an insert expressed in terms of a SELECT; the
// "VALUES" construct commonly used when inserting new rows singly is not supported).  If you are
// intending to use customHQL, we recommend that you avoid doing so for +link{operationBinding}s
// with +link{OperationBinding.operationType,operationType} "add", unless you have a special
// requirement such as a bulk insert; if you need custom queries to perform inserts
// on "hibernate" dataSources, we recommend you use +link{OperationBinding.customSQL,customSQL},
// which is valid for "hibernate" DataSources as well as "sql" dataSources.
// <p>
// For other operations on "hibernate" dataSources, however, HQL has the advantage of
// being more portable across different database engines than is plain SQL.
// <p>
// Note that using customHQL affects paging implementation. If you use it, full data set is fetched
// from Hibernate and records that aren't in the requested range are dropped at the server side.
//
// @group customQuerying
// @see OperationBinding.customSQL
// @see DataSourceField.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.customJQL (String : null : [IR])
// <b>This feature is available with Power or better licenses only.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// For a dataSource of +link{DataSource.serverType,serverType} "jpa", this
// property can be specified on an operationBinding to indicate that the server should run
// user-specified JQL (Java Persistence Query Language).
// <p>
// Note that inserting new records via JQL is often impractical, due to intentional restrictions
// in the language (it is only possible to perform an insert expressed in terms of a SELECT; the
// "VALUES" construct commonly used when inserting new rows singly is not supported).  If you are
// intending to use customJQL, we recommend that you avoid doing so for +link{operationBinding}s
// with +link{OperationBinding.operationType,operationType} "add", unless you have a special
// requirement such as a bulk insert; if you need custom queries to perform inserts
// on "jpa" dataSources, we recommend you use +link{OperationBinding.customSQL,customSQL},
// which is valid for "jpa" DataSources as well as "sql" dataSources.
// <p>
// For other operations on "jpa" dataSources, however, JQL has the advantage of
// being more portable across different database engines than is plain SQL.
// <p>
// Note that using customJQL affects paging implementation. If you use it, full data set is fetched
// from JPA and records that aren't in the requested range are dropped at the server side.
//
// @group customQuerying
// @see OperationBinding.customSQL
// @see DataSourceField.customSQL
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.skipRowCount (boolean : null : [IR])
// A SQLDataSource will normally issue two queries for a "fetch" operation when paging is
// enabled: one to determine the total rows available (the "row count query"), and one to fetch
// the specific range of rows requested.
// <p>
// Setting skipRowCount="true" will avoid the "row count query", but as a consequence
// +link{dsResponse.totalRows} will be set to match the requested +link{dsRequest.endRow} since
// the totalRows is unknown.
// <P>
// As an alternative, consider enabling
// +link{dataSource.progressiveLoading,progressive loading}, which avoids doing a query for row
// counts, but will still allow the user to load more results using the scrollbar if viewing
// results in a ListGrid.
//
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.customFields (String or Array: null : [IR])
// Indicates that the listed fields should be included in the default
// +link{selectClause,selectClause} and +link{selectClause,whereClause} generated for this
// operationBinding, even if they are marked +link{dataSourceField.customSQL,customSQL}="true".
// <P>
// If you need to apply different sets of overrides for the <code>selectClause</code> and the
// <code>whereClause</code>, use +link{customValueFields,customValueFields} and/or
// +link{customCriteriaFields,customCriteriaFields} instead.  If you specify both
// <code>customFields</code> and <code>customCriteriaFields</code> or
// <code>customValueFields</code>, the more specific variant wins.  If you specify both
// <code>customFields</code> and +link{excludeCriteriaFields,excludeCriteriaFields},
// <code>customFields</code> wins (this is another use case when you may wish to use
// <code>customValueFields</code> instead)
// <p>
// You can specify this property as a comma-separated list (eg, "foo, bar, baz") or by just
// repeating the &lt;customFields&gt; tag multiple times with one field each.
// <p>
// This property is only applicable to DataSources of +link{dataSource.serverType,"sql"}.
//
// @group customQuerying
// @see OperationBinding.customValueFields
// @see OperationBinding.customCriteriaFields
// @see OperationBinding.excludeCriteriaFields
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.customValueFields (String or Array: null : [IR])
// Indicates that the listed fields should be included in the default
// +link{selectClause,selectClause} generated for this operationBinding, even if they are marked
// +link{dataSourceField.customSQL,customSQL}="true".
// <P>
// You can specify this property as a comma-separated list (eg, "foo, bar, baz") or by just
// repeating the &lt;customValueFields&gt; tag multiple times with one field each.
// <p>
// This property is only applicable to DataSources of +link{dataSource.serverType,"sql"}.
//
// @group customQuerying
// @see OperationBinding.customFields
// @see OperationBinding.customCriteriaFields
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.customCriteriaFields (String or Array : null : [IR])
// Indicates that the listed fields should be included in the default
// +link{whereClause,whereClause} generated for this operationBinding, even if they are marked
// +link{dataSourceField.customSQL,customSQL="true"}.
// <P>
// You can specify this property as a comma-separated list (eg, "foo, bar, baz") or by just
// repeating the &lt;customCriteriaFields&gt; tag multiple times with one field each.
// <p>
// This property is only applicable to DataSources of +link{dataSource.serverType,"sql"}.
//
// @group customQuerying
// @see OperationBinding.customFields
// @see OperationBinding.customValueFields
// @see OperationBinding.excludeCriteriaFields
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.excludeCriteriaFields (String or Array: null : [IR])
// Indicates that the listed fields should be excluded from the default
// +link{whereClause,whereClause} generated for this operationBinding.
// <P>
// This enables you to use these fields in a +link{group:customQuerying,custom query} while
// still allowing the $defaultWhereClause to be generated for all other fields.  For example,
// you might take a particular field and apply it in the WHERE clause of a subquery.
// <p>
// You can specify this property as a comma-separated list (eg, "foo, bar, baz") or by just
// repeating the &lt;customCriteriaFields&gt; tag multiple times with one field each.
// Note that if a field is included in both excludeCriteriaFields and
// +link{operationBinding.customCriteriaFields,customCriteriaFields}, customCriteriaFields
// wins.
// <p>
// This property is only applicable to DataSources of +link{dataSource.serverType,"sql"}.
//
// @group customQuerying
// @see OperationBinding.customCriteriaFields
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.useForCacheSync (boolean : true : [IR])
// For an operationBinding of +link{operationType,operationType} "fetch" which specifies no
// +link{operationId,operationId}, this property determines whether the operationBinding
// should be used for cache synchronization purposes (ie, to retrieve the record most recently
// added or changed).  This property has no effect on an operationBinding that specifies an
// operationId - see +link{cacheSyncOperation}.
// <p>
// In order to work correctly with SmartClient's cache synchronization system, an
// operationBinding marked useForCacheSync should have the following properties:
// <ul>
// <li>Able to complete its retrieval using no context other than the values of the primary key
// fields declared in the dataSource (these will be provided in the $criteria object passed to
// the operation)</li>
// <li>Returns the entire record, including any values that may require joins to other tables
// or other complexities</li>
// </ul>
// This property is only applicable to DataSources of type "sql".
//
// @group customQuerying
// @see OperationBinding.cacheSyncOperation
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.cacheSyncOperation (String : null : [IR])
// For an operationBinding of +link{operationType,operationType} "add" or "update", this
// property is the +link{operationId,operationId} of a "fetch" operationBinding to use
// for cache synchronization purposes (ie, to fetch the row most recently added or changed).
// This property, along with +link{useForCacheSync,useForCacheSync} and
// +link{canSyncCache,canSyncCache} is provided so that you can use custom database operations
// without sacrificing the benefits of SmartClient's automatic cache synchronization.
// <p>
// This property is only applicable to DataSources of type "sql".
//
// @group customQuerying
// @see OperationBinding.useForCacheSync
// @see OperationBinding.canSyncCache
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.relaxSecurityForCacheSync (Boolean : null : [IR])
// For a "fetch" operationBinding which is to be used as the cache-sync operation for one or
// more "add" or "update" operations, set this flag to prevent the server from applying any
// +link{requiresRole,declarative security} rules when this operation is run <em>for cache-sync
// purposes</em>.  If the operation is run as a normal client request, all normal security
// processing applies regardless of this flag.
// <p>
// This property can help to avoid the need for coded workarounds in some security edge cases.
// For example, consider a secure application that only allows authenticated users; this
// application would naturally specify
// +link{dataSource.requiresAuthentication,requiresAuthentication}: <code>true</code> at the
// DataSource level, to enforce that requirement.  However, this causes a problem when a new
// user registers: we can provide a special "add" operation just for registering, that does
// not require authentication, but that is going to cause a cache-sync fetch that <b>does</b>
// require authentication, and that is going to fail because the user is not yet authenticated.
// <p>
// We could provide a special cache-sync fetch operation that does not require authentication,
// and hook it up to the special registration operation via the
// +link{cacheSyncOperation,cacheSyncOperation} setting.  However, that operation can also be
// executed from ordinary client code, leading to a situation where malicious code could
// obtain user details without being authenticated.
// <p>
// For this use case, the best solution is to create a special cache-sync fetch operation as
// described, but to specify normal security settings.  Then, specify
// <code>relaxSecurityForCacheSync</code> on that operationBinding to make the system skip
// security checks only when the operation is actually executing as part of a cache-sync fetch.
//
// @group customQuerying
// @see OperationBinding.cacheSyncOperation
// @see OperationBinding.useForCacheSync
// @see OperationBinding.canSyncCache
// @serverDS only
// @visibility internal
//<

//> @attr operationBinding.canSyncCache (Boolean : null : [IR])
// For an operation of type "add" or "update", a SQLDataSource will normally obtain
// data to return to the client by performing the "cacheSyncOperation": a SELECT statement that
// retrieves the modified record by primary key.  This accommodates sequence columns, columns
// with default values, database triggers and other database features that may modify data
// after insertion or update.
// <P>
// Certain major SQL customizations can prevent the SQLDataSource from authoritatively
// determining the primary key used in the SQL statement, such that re-selecting the saved
// record may fail.  By default, when <code>canSyncCache</code> has not been explicitly
// set, in the following cases it is assumed that the normal cacheSyncOperation cannot be used:
// <ul>
// <li> &lt;customSQL&gt; has been used to define an entirely custom query
// <li> a custom &lt;whereClause&gt; has been defined for an "update" or "remove" operation
// <li> a custom &lt;valuesClause&gt; has been defined for an "add" operation
// </ul>
// If any of these cases apply or if <code>canSyncCache</code> has been set false, the server
// will skip the cacheSyncOperation and return a DSResponse where
// +link{dsResponse.invalidateCache} has been set to true to notify client-side components that
// they may need to refresh their entire cache.
// <P>
// Alternatively, if the default re-selection behavior will not work but a customized SQL
// query would work, you can define that SQL operation as another operationBinding and use
// +link{operationBinding.cacheSyncOperation} to declare that it should be used.  Setting
// <code>cacheSyncOperation</code> implicitly sets <code>canCacheSync</code> to true.
// <P>
// This property is only applicable to DataSources of type "sql".
//
// @group customQuerying
// @see OperationBinding.useForCacheSync
// @see OperationBinding.cacheSyncOperation
// @serverDS only
// @visibility customSQL
//<

//> @attr operationBinding.sqlType (SQLType : null : [IR])
// For dataSources of +link{DataSource.serverType,serverType} "sql" and "hibernate" only, this
// property determines whether "custom" operations have their custom SQL or HQL sent to the
// underlying database via a JDBC <code>executeQuery()</code> or a JDBC
// <code>executeUpdate()</code>.  The default value of null means the same as "query", so you
// only need to use this property when your custom SQL or HQL updates data.
//
// @group customQuerying
// @serverDS only
// @visibility customSQL
//<

//> @type SQLType
// The types of custom query that can be handled by SmartClient's built-in "sql" and
// "hibernate" DataSources.  Note, only applies to +link{operationBinding.operationType}
// "custom".
//
// @value "query"   The custom SQL or HQL is read-only
// @value "update"  The custom SQL or HQL updates data
//
// @group customQuerying
// @visibility customSQL
//<


// Miscellaneous
// --------------------------------------------------------------------------------------------

//> @attr operationBinding.allowMultiUpdate (boolean : null : [IR])
// Ordinarily, "update" and "remove" operations are only allowed for +link{class:DataSource}s
// that have a +link{attr:DataSourceField.primaryKey,primaryKey}, and all primary key values
// are present in the request.  This is because an update of a DataSource with no primary key,
// or an update request that has missing primary key values, cannot be guaranteed to affect
// only one record.
// <p>
// Setting this property on an operationBinding circumvents this restriction for that operation
// only.
// <p>
// <b>Warning:</b> Be aware that this is a potentially dangerous setting and should be used
// with care.  With this flag set, you have no guarantee that an update will not change or
// remove every row in a table.
// <p>
// Also, running <code>allowMultiUpdate</code> operations directly from the client is not
// straightforward because it requires the ability to specify criteria and values separately
// in the request, which is not currently supported.  This can be worked around in various
// ways, but really <code>allowMultiUpdate</code> is primarily intended for server-side
// operations.  Therefore, the recommended pattern is to use a
// +link{dataSource.performCustomOperation,custom operation} from the client to invoke a DMI on
// the server which performs the multi-update operation via a second, server-side DSRequest.
//
// @serverDS only
// @visibility external
//<





//> @attr operationBinding.qualifyColumnNames (boolean : true : [IR])
// Specifies, for this specific operationBinding, whether to qualify column names with table
// names in any SQL we generate.  Overrides the +link{DataSource.qualifyColumnNames} property.
// Only applicable to dataSources of +link{DataSource.serverType,serverType} "sql".
//
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.invalidateCache (boolean : null : [IR])
// If set, every invocation of this operationBinding will invalidate the local cache, forcing
// a server visit to refresh the data.
//
// @serverDS allowed
// @visibility external
//<

//> @attr operationBinding.outputs (String: null : [IR])
// Specifies, for this operationBinding only, the list of field names that should be returned
// to the client.  Typically this will be a subset of the +link{DataSource.fields}, but note
// that this is not a requirement; <code>outputs</code> can include fields that are not defined
// in the DataSource's field list.  In this case, the server will return extra fields even if
// +link{dataSource.dropExtraFields} is true.
// <p>
// You specify this property as a string containing a comma-separated list of field names
// (eg, "foo, bar, baz")
//
// @serverDS only
// @visibility external
//<


//> @attr operationBinding.sqlPaging (SQLPagingStrategy : null : IRW)
// The paging strategy to use for this specific OperationBinding.  If this property is not
// set, we fall back to the +link{DataSource.sqlPaging} value, and the defaults described in
// the documentation for that property.
//
// @see dataSource.sqlPaging
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.sqlUsePagingHint (boolean : null : IR)
// If explicitly set true or false, forces the use of a "hint" in the SQL we generate for paged
// queries on or off as appropriate.  If not set, defaults to the
// +link{dataSource.sqlUsePagingHint} value.  Note this property is only applicable to
// +link{dataSource.serverType,SQL} DataSources, only when a
// +link{dataSource.sqlPaging,paging strategy} of "sqlLimit" is in force, and it only has an
// effect for those specific database products where we employ a native hint in the generated
// SQL in an attempt to improve performance.
//
// @requiresModules SCServer
// @see dataSource.sqlUsePagingHint
// @group sqlPaging
// @serverDS only
// @visibility external
//<


//> @attr operationBinding.useSubselectForRowCount (boolean : null : IRW)
// Whether to use the subselect technique (see +link{DataSource.useSubselectForRowCount} for
// details) to derive a rowcount query for this operation.  If this property is not
// set, we fall back to the <code>useSubselectForRowCount</code> setting on the DataSource,
// and the defaults described in the documentation for that property.
//
// @see dataSource.useSubselectForRowCount
// @see operationBinding.customSQL
// @serverDS only
// @visibility external
//<



//> @attr operationBinding.progressiveLoading (boolean : null : IRW)
// Sets +link{DataSource.progressiveLoading,progressive loading mode} for this particular
// operation, overriding the DataSource-level setting.  Note that this setting applies only
// to fetch operations - it has no effect if specified on any other kind of operation.
//
// @see dataSource.progressiveLoading
// @serverDS only
// @group progressiveLoading
// @visibility external
//<


//> @attr operationBinding.allowAdvancedCriteria (boolean : null : IRWA)
// This property indicates whether this operation supports AdvancedCriteria. This setting
// overrides +link{dataSource.allowAdvancedCriteria} for this operation only. See
// +link{dataSource.supportsAdvancedCriteria()} for further information.
// <p>
// <b>NOTE:</b> If you specify this property in a DataSource descriptor
// (<code>.ds.xml</code> file), it is enforced on the server.  This means that if you run
// a request containing AdvancedCriteria against an OperationBinding that advertises itself
// as <code>allowAdvancedCriteria:false</code>, it will be rejected.
// @see dataSource.allowAdvancedCriteria
// @serverDS allowed
// @visibility external
//<



//> @object DSRequestModifier
// <b>Some elements of this feature are only available with Power or better licenses.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// An object that modifies a dsRequest based on several available
// values, including preceding responses from the same queue. You provide a list of
// DSRequestModifiers as the +link{operationBinding.criteria} or +link{operationBinding.values}
// of an +link{OperationBinding}.
// <P>
// A DSRequestModifier consists of a +link{dsRequestModifier.fieldName,fieldName}, usually a
// +link{dsRequestModifier.value,value} and possibly an +link{dsRequestModifier.operator,operator}
// and +link{dsRequestModifier.start,start} and/or +link{dsRequestModifier.end,end} values
// (applicable to advanced criteria only).  The value, start and end settings can be static, or -
// with Power or better licenses - they can be expressions in the Velocity template language,
// which will be resolved at runtime, immediately before the DSRequest is executed.
//
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS only
// @visibility external
//<

//> @attr dsRequestModifier.fieldName (String : null : [IR])
// The name of the field to add or replace on the DSRequest - whether this appears in the DSRequest's
// values or criteria depends on whether this is part of a +link{operationBinding.values} or
// +link{operationBinding.criteria} attribute.
//
// @serverDS only
// @visibility external
//<

//> @attr dsRequestModifier.operator (String : null : [IR])
// The name of the operator to apply when constructing criteria.  This property only applies
// to criteria; it is ignored if specified as part of a +link{operationBinding.values}
// attribute.
//
// @serverDS only
// @visibility external
//<

//> @attr dsRequestModifier.value (String : null : [IR])
// The value to assign to the field named by +link{fieldName}.  This value can be static, and
// for Pro licenses that is the only option.  With Power and better licenses, this value
// can be an expression in the Velocity template language.  In this latter case, there are a
// number of context variables available to you:
// <ul><li><b>dsRequest</b>. The DSRequest object being processed</li>
// <li><b>servletRequest</b>. The HttpServletRequest object representing the queue of requests
// sent from the client</li>
// <li><b>responseData</b>. The data member of a DSResponse object associated with an earlier
// DSRequest in the same queue</li></ul>
// (Note that the standard Velocity context variables provided by SmartClient Server are also
// available to you - see +link{group:velocitySupport}).
// <p>
// The most interesting of these is the last. An example of when this might be useful would be a
// queued batch of "add" operations for an order header and its details - the detail additions need
// to know the unique primary key that was assigned to the order.  This value will be in the
// response to the DSRequest that added the order header.  See this example:
// +explorerExample{queuedAdd}.
// <p>
// You can obtain the responseData of prior responses using the following Velocity syntax:<p>
// <code>$responseData.first.myField</code> is the myField property of the first response in the queue<br>
// <code>$responseData.first('order').myField</code> is the myField property of the first response to an
// operation (any operation) on the "order" DataSource<br>
// <code>$responseData.first('order', 'add').myField</code> is the myField property of the first
// response to an "add" operation on the "order" DataSource<p>
// All of these syntactic variations are also available on the <code>$responseData.last</code> object -
// "last" here meaning the most recent response matching the DataSource and operation type (if
// applicable).  Please see the server-side Javadoc for the
// <code>com.isomorphic.velocity.ResponseDataHandler</code> class.
//
// @see group:velocitySupport
// @serverDS only
// @visibility external
//<

//> @attr dsRequestModifier.start (String : null : [IR])
// The value to use for the start of a range.  This property only applies to criteria, and it
// only applies to operators of type "rangeCheck" - for example, the "between" operator.
// It is ignored if specified as part of a +link{operationBinding.values} attribute, or for
// an inapplicable operator type.
// <p>
// The same rules apply to this attribute as apply to +link{dsRequestModifier.value,value}, so
// you can use Velocity expressions if you have a Power or better license.
//
// @serverDS only
// @visibility external
//<

//> @attr dsRequestModifier.end (String : null : [IR])
// The value to use for the end of a range.  This property only applies to criteria, and it
// only applies to operators of type "rangeCheck" - for example, the "between" operator.
// It is ignored if specified as part of a +link{operationBinding.values} attribute, or for
// an inapplicable operator type.
// <p>
// The same rules apply to this attribute as apply to +link{dsRequestModifier.value,value}, so
// you can use Velocity expressions if you have a Power or better license.
//
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.criteria (Array of DSRequestModifier : null : [IR])
// <b>Elements of this feature are only available with Power or better licenses.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// A list of +link{DSRequestModifier}s that will be used to modify the criteria of each
// +link{DSRequest} that uses this operationBinding.  Note that the criteria elements are
// applied to DSRequest criteria as follows:
// <ul>
// <li><b>Simple criteria:</b> The field and value are just applied as an extra key/value
// pair in the criteria map, as long as the +link{dsRequestModifier.operator,operator}
// attribute is left unset, or is set to "equals".  For any other setting of
// <code>operator</code>, the criteria is first converted to the equivalent AdvancedCriteria
// and then processed as described below</li>
// <li><b>AdvancedCriteria:</b> If the topmost operator is "and", we add the new criterion
// as an additional criterion directly in the existing list.  Otherwise, we create a new
// top-level AdvancedCriteria with an operator of "and".  This is then set to have two
// elements in its criteria: the previous top-level criteria and the new criterion.</li>
// </ul>
// The effect of this is to apply any criteria specifed here as additional constraints on top
// of what the user has specified, and of course, the user is unable to affect this.  Thus,
// this is a suitable and convenient place to enforce rules such as "Users can only ever see
// their own records".
// <p>Below is an example of the XML as it should be defined in your ds.xml, datasource
// definitions. <code>
// &lt;operationBindings&gt;
//   &lt;operationBinding operationType="fetch" operationId="..."&gt;
//     &lt;criteria fieldName="USER_ROLE" value="ADMIN" operator="equals" /&gt;
//   &lt;/operationBinding&gt;
// &lt;/operationBindings&gt;</code>
//
// @see operationBinding.values
// @serverDS only
// @visibility external
//<

//> @attr operationBinding.values (Array of DSRequestModifier : null : [IR])
// <b>Elements of this feature are only available with Power or better licenses.</b> See
// <a href=http://smartclient.com/product>smartclient.com/product</a> for details.
// <p>
// A list of +link{DSRequestModifier}s that will be used to modify the values object of each
// +link{DSRequest} that uses this operationBinding. See this example:
// +explorerExample{queuedAdd}.
// <p>Below example of the xml as it should be defined in ds.xml: <code>
// &lt;operationBinding operationType="add"&gt;
//   &lt;values fieldName="orderID" value="$responseData.last('queuedAdd_order','add').orderID" /&gt;
// &lt;/operationBinding&gt;</code>
//
// @see operationBinding.criteria
// @serverDS only
// @visibility external
//<

//> @type XPathExpression
// A standard XPath expression as a string. To learn about XPath, try the following search:
// <a href="http://www.google.com/search?q=xpath+tutorial" target="_blank"
// >http://www.google.com/search?q=xpath+tutorial</a>
// @visibility external
//<

//> @type VelocityExpression
// An expression in the <a href=http://velocity.apache.org/engine/releases/velocity-1.5/user-guide.html>
// Velocity Template Language</a> (VTL).  For more information on SmartClient's Velocity support,
// see +link{group:velocitySupport, Velocity support}.
// <p>
// Note that a <code>VelocityExpression</code> must often evaluate to a particular type of value
// to be useful.  For example, +link{DataSource.requires} must evaluate to true or false
// (Boolean objects or strings containing those two words), and +link{Mail.messageData} must
// evaluate to a Java <code>Map</code> object, or a Java <code>List</code> containing only
// <code>Map</code>s.
//
// @group velocitySupport
// @treeLocation Client Reference/Data Binding/DataSource
// @visibility external
//<

// Declarative security
// -----------------------------------------------------------------------------------------
//> @attr operationBinding.requiresAuthentication (boolean : null : IR)
// Whether a user must be authenticated in order to access this operation.  For details of
// what is meant by "authenticated", see +link{DataSource.requiresAuthentication}.
// <P>
// To protect access to an entire operationType (eg, all "fetch" operations), declare an
// operationBinding with <code>requiresAuthentication="true"</code>, +link{operationType} set
// to the operationType to be protected, but no +link{operationId}.  This will then
// prevent access to the "fetch" operationType unless another +link{operationBinding}
// declares requiresAuthentication="false" with a specific
// +link{operationBinding.operationId,operationId}.
//
// @requiresModules SCServer
// @serverDS only
// @group auth
// @visibility external
//<

//> @attr operationBinding.requiresRole (String : null : IR)
// Comma-separated list of user roles that are allowed to invoke the operation described by
// this operationBinding. If the current user has any of the roles listed, they can
// invoke the operation. Also note that <code>authentication.superuserRole</code> can be
// specified in the +link{group:server_properties,server.properties} file. If set this denotes a "super user"
// role - any user with that role will have access to all operations, regardless of the
// "requiresRole" settings for the operation.
// <P>
// Whether the current user has a given role is determined by calling the standard Java
// servlets method <code>httpServletRequest.isUserInRole()</code>, hence works with both simple
// J2EE security (realms and form-based authentication) and JAAS (Java Authentication &
// Authorization Service).
// <P>
// If you wish to use a role-based security scheme that does not make use of the servlet API's
// standards, SmartClient Server also implements the <code>setAuthenticated</code> and
// <code>setUserRoles</code> methods on <code>RPCManager</code>.
// You can use this API to tell SmartClient that all the requests in the queue currently
// being processed are associated with a user who has the roles you supply; in this case,
// SmartClient will not attempt to resolve the user's roles via
// <code>httpServletRequest.isUserInRole()</code>. When taking this approach, the
// <code>rpcManager.setUserRoles()</code> method should be called on the server for
// each transaction received from the client. We recommend doing this by overriding the
// special IDACall servlet and checking server side state to determine the current user's
// roles, calling the API, and then calling <code>handleDSRequest()</code> or
// <code>handleRPCRequest()</code> directly to handle the request(s) passed in.<br>
// Here's an example of this approach which assumes the current user's roles has been
// set directly on the HttpSession object as a comma-separated-string attribute "roles":
// <pre><code>
//  public class SecureIDACall extends IDACall {
//
//      public void processRequest(HttpServletRequest request,
//              HttpServletResponse response)
//       throws ServletException, IOException
//      {
//          HttpSession session = request.getSession();
//          Object roles = session == null ? null : session.getAttribute("roles");
//
//          if (roles != null) {
//              try {
//                  RequestContext context = RequestContext.instance(this, request, response);
//                  RPCManager rpc = new RPCManager(request, response);
//                  rpc.setAuthenticated(true);
//                  rpc.setUserRoles((String) roles);
//
//                  // call processRPCTransaction() to iterate through all RPCRequests and
//                  // DSRequests and execute them
//                  processRPCTransaction(rpc, context);
//
//              } catch (Throwable e) {
//                  handleError(response, e);
//              }
//          } else {
//              super.processRequest(request, response);
//          }
//      }
//  }
// </code></pre>
// <P>
// If there is an operationBinding declared for a given operationType which does not have an
// +link{operationId}, that is, it is the default operationBinding for the type, then any other
// operationBinding of the same type is assumed to have the same setting for
// <code>requiresRole</code> as the default operationBinding for the operationType.  For
// example, given these declarations:
// <pre>
//     &lt;operationBinding operationType="fetch" requiresRole="manager"&gt;
//           ... settings ...
//      &lt;/operationBinding&gt;
//     &lt;operationBinding operationType="fetch" operationId="fetchWithExtraFields"&gt;
//           ... settings ...
//      &lt;/operationBinding&gt;
// </pre>
// The second operationBinding requires the "manager" role even though there is no explicit
// <code>requiresRole</code> declaration.  To prevent the "manager" role being required by the
// second operationBinding, add <code>requireRole=""</code>.
// <P>
// Note that if +link{dataSource.requiresRole} is set, all operations on the DataSource require
// the roles set for the DataSource as a whole, even if they declare individual
// <code>requiresRole</code> attributes.
// <P>
// This property is valid only for a server-side DataSource when using the SmartClient Server.
//
// @serverDS only
// @requiresModules SCServer
// @group auth
// @visibility external
//<

//> @attr operationBinding.requires (VelocityExpression : null : IR)
// Indicates that the specified +link{type:VelocityExpression} must be true for a user to access
// this operationBinding.
// <P>
// As with +link{operationBinding.requiresRole}, if there an operationBinding that is the
// default operationBinding for the operationType, its <code>requires</code> expression is
// assumed to apply to all other operationBindings of the same type unless they explicitly set
// <code>requires=""</code>
// <P>
// +link{DataSource.requires}, if specified, applies before
// <code>operationBinding.requires</code> is evaluated.  In this case, both <code>requires</code>
// expressions must be true for the request to be accepted.
//
// @serverDS only
// @group auth
// @visibility external
//<


//> @attr operationBinding.creatorOverrides (boolean : null : IR)
// Indicates that field-level declarative security rules are waived for rows that were
// created by the current user, as described in the discussion of
// +link{DataSource.creatorOverrides,dataSource.creatorOverrides}.  This setting overrides
// <code>dataSource.creatorOverrides</code>, for this operation only.
//
// @requiresModules SCServer
// @see dataSourceField.editRequires
// @see dataSourceField.viewRequires
// @see dataSource.creatorOverrides
// @group fieldLevelAuth
// @serverDS only
// @visibility external
//<


// Template-based email
// -----------------------------------------------------------------------------------------

//> @attr operationBinding.mail (Mail : null : IR)
// Definition of an email message that will be sent as an after-effect of selecting or updating
// data.
// <p>
// Note that if a fixed number of different messages need to be sent, multiple
// <code>&lt;mail&gt;</code> tags may be specified.  For example, one mail could be sent to an
// admin address, and a different message to every member of a user group.
//
// @visibility external
// @serverDS only
// @group mail
//<

//> @class Mail
// A <code>Mail</code> object describes an email that will be sent automatically by the
// SmartClient Server as part of a normal +link{class:DataSource} operation.  Mail definitions
// are part of an +link{class:operationBinding}, and they can only be specified in a
// DataSource's XML definition.  Despite the declarative nature of mail definitions, templating
// support via Velocity makes it very flexible; it is very easy to include values from records
// just fetched from permanent storage by this operation, for example.  See
// +link{group:velocitySupport} for an overview of SmartClient Server's pervasive support for
// templated declarative operations using Velocity.
// <P>
// The default context for the message is the dsResponse.data; that is, $foo in the
// message refers to the field "foo" in the DataSource response (the equivalent of
// calling dsResponse.getFieldValue("foo") in Java).  You also have access to the normal
// Velocity variables described in the overview linked to above.
// <P>
// If multiple records appear in the dsResponse, one message will be sent per record
// in the response unless "multiple" is set to false.  This allows a set of
// recipients to be selected by a "fetch" operationBinding and messages sent to each.
// This also means that if no data is selected by a fetch or affected by an update,
// no mail is sent.
// <P>
// If you have the Transaction Chaining feature, all of the variables normally
// accessible in other Velocity contexts are available in the message template as
// well, for example, $responseData.last('order').orderId.  See
// +link{dsRequestModifier.value,transaction chaining} for more details.
// <p>
// You can provide the body of the email directly in the +link{mail.messageTemplate,messageTemplate}
// tag, or you can provide a filename in the +link{mail.templateFile,templateFile} attribute,
// which tells SmartClient to use the contents of that file as the body of the email.  If you
// provide neither of these, your email will have no body; if you provide both, SmartClient will
// ignore the file and just use the content directly provided.
// <p>
// <b>Mail server configuration</b><br>
// The mail server to use for sending emails is configured in the +link{group:server_properties,server.properties}
// file.  The following values can be provided:<p>
// <code>mail.system.mail.smtp.host</code>: The name of the SMTP server to use; defaults
// to "localhost"<br>
// <code>mail.system.mail.smtp.port</code>: What port is the MTA listening on; defaults to 25<br>
// <code>mail.system.mail.smtp.auth</code>: Whether this server requires authentication;
// defaults to false<br>
// <code>mail.system.mail.smtp.user</code>: SMTP user, if authentication is on; no default<br>
// <code>mail.system.mail.smtp.password</code>: Password, if authentication is on; no default<br>
// <p>
// Note that we will also pass any other properties that start <code>mail.system</code> through
// to the underlying Javamail <code>Session</code> object, so you can use Javamail features
// that aren't explicitly exposed by SmartClient's mail support.  For example, many cloud-based
// SMTP providers require that you issue a STARTTLS command before authenticating; you can
// achieve this by adding the following line to +link{group:server_properties,server.properties}:<p>
// <code>mail.system.mail.smtp.starttls.enable: true</code>
//
// @treeLocation Client Reference/Data Binding/DataSource
// @visibility external
// @serverDS only
// @group mail
//<

//> @attr mail.templateFile (URL : null : IR)
// Qualified name of the file containing the message template, relative to webroot.  Like all
// the other <code>String</code> properties of <code>Mail</code>, you can use Velocity
// substitution variables in this property.  The content itself (ie, the contents of the file
// named in this property) can also contain Velocity substitutions, of course.
// <p>
// You must specify either this property or +link{mail.messageTemplate,messageTemplate}, but
// clearly it makes no sense to specify them both.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.messageTemplate (String : null : IR)
// The text that will form the body of the mail message.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// You must specify either this property or +link{mail.templateFile,templateFile}, but clearly
// it makes no sense to specify them both.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.to (String : null : IR)
// Comma-separated list of recipients.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// If you specify a "To:" line in the body of your email, recipient addresses will be parsed
// and this property will be ignored.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.cc (String : null : IR)
// Comma-separated list of "CC" recipients.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// If you specify a "Cc:" line in the body of your email, recipient addresses will be parsed
// and this property will be ignored.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.bcc (String : null : IR)
// Comma-separated list of "BCC" recipients.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// If you specify a "Bcc:" line in the body of your email, recipient addresses will be parsed
// and this property will be ignored.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.from (String : null : IR)
// The email address the message will appear to come from.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// If you specify a "From:" line in the body of your email, the from address will be parsed
// and this property will be ignored.
// <p>
// Ordinarily, this property (or the parsed equivalent in the email body) is mandatory for all
// emails.  However, if you specify the property <code>mail.system.mail.smtp.from</code> in
// <code>server.properties</code>, it will become the default "from" address, and this property
// becomes optional.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.replyTo (String : null : IR)
// An email address to indicate as the reply-to address on the message.  Like all other
// <code>String</code> properties of <code>Mail</code>, you can use Velocity substitution
// variables in this property.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.subject (String : null : IR)
// The string to appear in the subject line of the message.  Like all other <code>String</code>
// properties of <code>Mail</code>, you can use Velocity substitution variables in this property.
// <p>
// If you specify a "Subject:" line in the body of your email, the subject will be parsed
// and this property will be ignored.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.multiple (boolean : null : IR)
// By default, multiple mail messages are sent if the dsResponse contains multiple records.
// Set this property to false to prevent this behavior.
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.messageData (VelocityExpression : null : IRA)
// Allows you to specify an alternative source of substitution values for the templating
// process (by default, the data record(s) returned by the DataSource operation are used).
// If set, this +link{type:VelocityExpression} should evaluate to a Java <code>Map</code>
// object, or a Java <code>List</code> containing only <code>Map</code>s.
// <p>
// For example, assuming you had suitable context stored in a <code>Map</code> that is
// held in the current session as attribute "foo", the following would cause the mail
// templating system to look in there for values when running substitution:
// <pre>
//     messageData="$session.foo"
// </pre>
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.contentType (String : null : IR)
// Used to override the content-type header applied to the email.  Primarily used to send
// HTML emails rather than plain text ones (use "text/html" to do this).
//
// @group mail
// @serverDS only
// @visibility external
//<

//> @attr mail.encoding (String : null : IR)
// Used to override the character encoding we apply to the email body.  If you do not set
// this property, it is derived from the +link{group:server_properties,server.properties} setting
// <code>mail.system.default.encoding</code>, which is set to "UTF-8" by default.  This
// setting should be appropriate for most use cases.
//
// @group mail
// @serverDS only
// @visibility external
//<




    // Schema handling
    // --------------------------------------------------------------------------------------------

    // look up a schema, prefering the local schemaNamespace if we're namespaced
    getSchema : function (schemaName, schemaType) {
        // if we have a SchemaSet, look up through that
        var schemaSet = this.getSchemaSet();
        if (schemaSet != null) {
            //this.logWarn("looking up schema: " + schemaName + " of kind: " + schemaType +
            //             " in schema namespace: " + this.schemaNamespace);

            var schema = schemaSet.getSchema(schemaName, schemaType);
            if (schema != null) return schema;
        }

        // look up schema through our WSDL web service, if we have one, to allow for multiple
        // SchemaSets within a WebService that reference between each other (SForce service is
        // like this, for the SObject type)
        var service = this.getWebService();
        if (isc.isA.WebService(service)) return service.getSchema(schemaName, schemaType);

        // finally, look for any globally defined schema
        return isc.DS.get(schemaName, null, null, schemaType);
    },

    // how to refer to record[s] of this DataSource
    getTitle : function () {
        return this.title || this.getAutoTitle(this.ID);
    },
    getPluralTitle : function () {
        return this.pluralTitle || (this.getTitle() + "s");
    },

    // get the field best used as a title for records from this DataSource
    defaultTitleFieldNames:[
        "title",
        "label",
        "name",
        "id"
    ],
    getTitleField : function () {
        if (this.titleField == null) {
            // if a title field hasn't been explicitly specified, take a guess.
            // Also, remember the guess (this is an inner loop)
            var fieldNames = isc.getKeys(this.getFields());
            var normalizedFieldNames = fieldNames.map("toLowerCase");
            for (var i = 0; i < this.defaultTitleFieldNames.length; i++) {
                var index = normalizedFieldNames.indexOf(this.defaultTitleFieldNames[i]);
                if (index != -1 && !this.getField(fieldNames[index]).hidden) {
                    this.titleField = fieldNames[index];
                    break;
                }
            }
            // Use the first (non hidden) fieldName
            if (this.titleField == null) {
                for (var i = 0; i < fieldNames.length; i++) {
                    if (this.getField(fieldNames[i]).hidden) continue;
                    this.titleField = fieldNames[i];
                    break;
                }
                if (this.titleField == null) this.titleField = fieldNames[0];
            }
        }
        return this.titleField;
    },

    getIconField : function () {
        var undef;
        if (this.iconField === undef) {
            this.iconField = null;

            // if an Icon field hasn't been explicitly specified, take a guess.
            // Also, remember the guess (this is an inner loop)
            var fieldNames = isc.getKeys(this.getFields());
            var iconFieldNames = ["picture", "thumbnail", "icon", "image", "img"];
            for (var i = 0; i < iconFieldNames.length; i++) {
                var fieldName = iconFieldNames[i],
                    field = this.getField(fieldName);
                if (field && isc.SimpleType.inheritsFrom(field.type,"image")) {
                    this.iconField = fieldName;
                }
            }
        }
        return this.iconField;
    },

    // init the various sources for a view
    //     this loads those datasources and creates a "fields" array that is
    //    the union of the fields coming from the other sources
    initViewSources : function () {
        var list = this.fields = {};

        for (var sourceName in this.sources) {
            var ds = isc.DS.get(sourceName);
            if (!ds) continue;

            var sourceFields = this.sources[sourceName].fields;
            for (var field in sourceFields) {
                var value = sourceFields[field],
                    item = null;

                if (value == "*") {
                    // if value is a "*", field is the name of the field in the datasource
                    item = ds.fields[field];
                } else if (isc.isA.String(value)) {
                    // if value is some other string, value is the name of some other field in ds
                    item = ds.fields[value];
                } else if (isc.isAn.Object(value)) {
                    // it's an object, first apply properties in ds[value.field], then apply
                    //    the properties in the value to override
                    item = isc.addProperties({}, ds.fields[ds.fields[value.field]]);
                    isc.addProperties(item, value);
                }

                if (item) list[field] = item;
            }
        }
    },

    inheritsSchema : function (ds) {
        if (ds == null) return false;
        if (isc.isA.String(ds)) ds = this.getSchema(ds);

        if (ds == this || ds == isc.DS.get("Object")) return true;

        if (!this.hasSuperDS()) return false;
        return this.superDS().inheritsSchema(ds);
    },

    getInheritedProperty : function (property) {
        if (this[property]) return this[property];
        var superDS = this.superDS();
        return superDS ? superDS.getInheritedProperty(property) : null;
    },

    hasSuperDS : function () {
        if (this.inheritsFrom) return true;
        return false;
    },

    superDS : function () {
        if (this.hasSuperDS()) return this.getSchema(this.inheritsFrom);
        return null;
    },

    //> @method dataSource.getField()
    // Return the field definition object.
    // @param fieldName (String) Name of the field to retrieve
    // @return (DataSourceField) field object
    // @visibility external
    //<
    getField : function (fieldName, checkDataPath) {
        if (isc.isAn.Object(fieldName)) fieldName = fieldName.name;
        var fields = this.getFields();
        var field = fields ? fields[fieldName] : null;

        if (field == null && checkDataPath && fields != null) {
            for (var i in fields) {
                if (fields[i] == null) continue;
                if (fields[i].dataPath == fieldName) {
                    field = fields[i];
                    break;
                }
            }
        }
        return field;
    },

    //> @method dataSource.getFieldForDataPath()
    // Return the field definition object corresponding to the supplied dataPath
    // @param dataPath (String) dataPath of the field to retrieve
    // @return (DataSourceField) field object, or null if no field corresponds to the
    //                           supplied dataPath
    // @visibility external
    //<
    getFieldForDataPath : function (dataPath) {
        if (isc.isAn.Object(dataPath)) dataPath = dataPath.dataPath;
        if (!dataPath) return null;
        var segments = dataPath.trim(isc.Canvas._$slash).split(isc.Canvas._$slash);
        var dataSource = this;
        for (var i = 0; i < segments.length; i++) {
            if (dataSource == null) return null;
            var field = dataSource.getField(segments[i]);
            if (!field) return null;
            dataSource = isc.DataSource.get(field.type);
        }
        return field;
    },

    // Given a dataPath referring to fields within nested dataSources, return the
    // dataSource associated with the dataPath records.
    // pathToRecord: Does the dataPath include the field within a record, or is it
    // the path to a record as a whole (for example the dataPath applied to an entire dataBoundComponent?)
    getDataSourceForDataPath : function (dataPath, pathToRecord) {
        if (isc.isAn.Object(dataPath)) dataPath = dataPath.dataPath;
        if (!dataPath) return null;
        var segments = dataPath.trim(isc.Canvas._$slash).split(isc.Canvas._$slash);
        var dataSource = this;
        // Note that we don't need to iterate all the way to the end of the dataPath, just to the
        // penultimate entry - we don't care about the type of the field itself, just the DS it's
        // nested inside.
        var length = pathToRecord ? segments.length : segments.length-1;
        for (var i = 0; i < length; i++) {
            var field = dataSource.getField(segments[i]);
            if (!field) {
                this.logInfo("getDataSourceForDataPath() - unable to find nested field:"
                        + segments[i] + " from dataSource:" + dataSource +
                        ". Orginal dataPath:" + dataPath, "dataBinding");
                return null;
            }
            var parentDS = dataSource;
            dataSource = isc.DataSource.get(field.type);
            if (dataSource == null) {
                this.logInfo("getDataSourceForDataPath() - unable to find nested dataSource for field:" +
                    field.name + " of type:" + field.type + " on dataSource:" + parentDS +
                    ". Original dataPath:" + dataPath, "dataBinding");
                break;
            }
        }
        return dataSource;

    },

    getFieldByTitle : function (fieldTitle) {
        var fields = isc.getValues(this.getFields());

        for (var i=0; i<fields.length; i++) {
            var field = fields[i],
                title = field.title || isc.DS.getAutoTitle(fields[i].name);
            if (title == fieldTitle) return field;
        }

        return null;
    },

    //> @method dataSource.getDisplayValue()
    // Given a fieldName and a dataValue, apply any +link{DataSourceField.valueMap} for the
    // field and return the display value for the field
    // @param fieldName (String) name of the field to retrieve a value for
    // @param value (any) data value for the field
    // @return (any) display value for the field
    // @visibility external
    //<
    getDisplayValue : function (fieldName, value) {
        var field = this.getField(fieldName);
        if (field == null) return value;
        if (isc.isAn.Object(field.valueMap) && !isc.isAn.Array(field.valueMap) &&
            isc.propertyDefined(field.valueMap, value))
        {
            return field.valueMap[value];
        }
        return value;
    },

    //> @method dataSource.getFieldNames()
    // Retrieves the list of fields declared on this DataSource.
    //
    // @param excludeHidden (boolean) If true, returns only those fields that are
    //                                not marked as hidden
    // @return (Array of String) names of all fields declared on this DataSource
    // @visibility external
    //<
    getFieldNames : function (excludeHidden) {
        if (isc._traceMarkers) arguments.__this = this;
        if (!excludeHidden) return isc.getKeys(this.getFields());
        var fields = this.getFields(),
            filtered = [],
            filteredIndex = 0;
        for (var fieldName in fields) {
            if (fields[fieldName] != null && !fields[fieldName].hidden) {
                filtered[filteredIndex++] = fieldName;
            }
        }
        return filtered;
    },

    // get the field defined on this DataSource only (as opposed to inherited)
    getLocalFields : function (dontResolveTypes) {
        if (this._builtinValidatorsAdded) return this.fields;
        if (dontResolveTypes) return this.fields;
        this._addTypeDefaults();
        this._autoDeriveTitles();
        this._builtinValidatorsAdded = true;
        return this.fields;
    },

    getFields : function () {
        if (isc._traceMarkers) arguments.__this = this;

        if (this.mergedFields) return this.mergedFields;

        //this.logWarn("field init for: " + this);

        // NOTE: silently ignore looping schema definitions.  Currently can happen with
        // schemaTranslator if an element is declared to be of a complexType named the same as
        // the element.
        if (!this.hasSuperDS() || this == this.superDS()) {
            return this.mergedFields = this.getLocalFields();
        }

        // Log a warning and bail if the superDS does not exist, in order to avoid a hard
        // crash a few lines further down when we dereference it
        // the element.
        if (!this.superDS()) {
            this.logWarn("DataSource " + this.ID + " inheritsFrom " + this.inheritsFrom +
                         ", but there is no DataSource of that name currently loaded. " +
                         "Ignoring the inheritsFrom declaration.");
            return this.mergedFields = this.getLocalFields();
        }

        // combine fields: local fields override superDS fields.


        // field inheritance flags:
        // - [default] local fields first, then parent fields
        // - useParentFieldOrder: fields are in parent order
        // - showLocalFieldsOnly: only local fields are shown.  Parent fields are present but
        //   hidden so that serialization is correct.  Typical setting when extending imported
        //   schema to create a DataSource that serves as a default field set for DataBound
        //   components
        // - restrictToLocalFields: use internally for xs:restriction, in which parent fields
        //   must really be dropped (not just hidden) if they aren't redeclared - they aren't
        //   allowed when serializing
        var superDS = this.superDS();

        /*
        // future plans: use combineFieldOrders, equivalent to
        // dataBoundComponent.useAllDataSourceFields:true, as default binding.

        if (!this.showLocalFieldsOnly && !this.restrictToLocalFields &&
            !this.useParentFieldOrder)
        {
            return this.mergedFields =
                this.combineFieldOrders(superDS.getFields(),
                                        isc.getValues(this.getLocalFields()));
        }
        */

        if (this.showLocalFieldsOnly || this.restrictToLocalFields) {
            this.useParentFieldOrder = false;
        }

        var localFields = isc.addProperties({}, this.getLocalFields()),
            fields;
        if (!this.useParentFieldOrder) {
            // put together fields by adding super fields to the local fields, so local fields
            // are first
            fields = localFields;
        } else {
            // put together fields by adding fields in superDS field order, and adding the rest
            // of the local fields at the end
            fields = {};
        }

        // if we have a superDS, add its fields here.  NOTE: we do this in this order so
        // that locally defined fields appear first in the merged list, which affects the
        // default display of editors
        var fieldNames = (this.restrictToLocalFields ? isc.getKeys(this.getLocalFields()) :
                                                       superDS.getFieldNames());
        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                localField = localFields[fieldName];

            if (localField != null) {
                var superField = superDS.getField(fieldName);
                //this.logWarn("local and super definition for field: " + fieldName +
                //             ", local field: " + this.echo(localField) +
                //             ", super field: " + this.echo(superField));
                // if a field is present in a sub-DS which is hidden in a superDS,
                // assume the intent was to expose it, unless "hidden='true'" is reiterated, or
                // some other property that would hide the field is specified.  Likewise for
                // visibility=internal.  Note, this doesn't apply in cases where where we are
                // inheriting because of the presence of autoDeriveSchema and/or schemaBean.
                // In that case, we want to inherit everything that is not expressly overridden
                // on this DS, since the only purpose of the parent DS in this case is to
                // provide default settings for this specific DS.

                if (superField.hidden && localField.hidden == null &&
                    !localField.inapplicable && !this.autoDeriveSchema && !this.schemaBean)
                {
                    //this.logWarn("dataSource: " + this +
                    //             " overrides hidden field: " + fieldName +
                    //             " in superDS: " + superDS);
                    localField.hidden = false;
                }
                if (superField.visibility != null && localField.visibility == null &&
                    !localField.inapplicable && !localField.hidden &&
                    superField.visibility == "internal")
                {
                    //this.logWarn("dataSource: " + this +
                    //             " overrides internal field: " + fieldName +
                    //             " in superDS: " + superDS);
                    localField.visibility = "external";
                }

                // Save a copy of the localField - it's about to be clobbered
                var localCopy = isc.addProperties({}, localField);

                // local and super field definition: combine with overrides
                fields[fieldName] = superDS.combineFieldData(localField, null, true);

                // Special override case: all fields have a title attribute, because one will
                // auto-derived from the name if necessary.  However, we do not want to use
                // the auto-derived title if a better one can be inherited
                if (localCopy._titleAutoDerived) fields[fieldName].title = superField.title;
            } else {
                // field definition in parent only
                if (this.showLocalFieldsOnly) {
                    fields[fieldName] = isc.addProperties({}, superDS.getField(fieldName));
                    fields[fieldName].hidden = "true";
                } else {
                    fields[fieldName] = superDS.getField(fieldName);
                }
            }
            // check off this local field as having appeared in the parent already
            if (this.useParentFieldOrder) delete localFields[fieldName];
        }
        // add fields not declared in the parent
        if (this.useParentFieldOrder) isc.addProperties(fields, localFields);

        // special rule for XML schema: an xs:restriction is like showLocalFieldsOnly *except*
        // that attributes need not be redeclared to be valid in the derived type.
        // NOTE field order doesn't matter here; attributes are always at the end.
        if (this.restrictToLocalFields && isc.Schema && isc.isA.Schema(this)) {
            var parentFieldNames = superDS.getFieldNames();
            for (var i = 0; i < parentFieldNames.length; i++) {
                var fieldName = parentFieldNames[i],
                    parentField = superDS.getField(fieldName);

                if (parentField.xmlAttribute) {
                    fields[fieldName] = fields[fieldName] || parentField;
                }
            }
        }

        return this.mergedFields = fields;
    },

    // helper method to tell us if a dataSource has fields. Helpful if a ds
    // doesn't define fields itself, but inherits them instead. Used in
    // listGrid.setFields
    hasFields : function () {
        if (this.fields) return true;
        else if (this.inheritsFrom) {
            // climb the inheritance chain and check for fields
            var ds = this;
            while (ds.inheritsFrom) {
                ds = isc.DataSource.get(this.inheritsFrom);
                if (ds.fields) return true;
            }
        }
        return false;
    },

    // get all simple type fields, throughout this DataSource and any sub-DataSources.
    // if path/pathProperty is passed, also construct a dataPath and return *copies* of the
    // discovered fields with the dataPath applied under "pathProperty".
    getFlattenedFields : function (flatFields, path, pathProperty) {
        flatFields = flatFields || {}; // if called recursively, just add new fields to the
                                       // existing listing
        var fieldNames = this.getFieldNames();

        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = this.getField(fieldName);
            if (!this.fieldIsComplexType(fieldName)) {
                // first field with a given name wins (not clear what's best here)
                if (flatFields[fieldName] == null) {
                    // label the field with the DataSource it came from
                    field.sourceDS = this.ID;
                    if (path) {
                        field = isc.addProperties({
                        }, field);
                        field[pathProperty] = path + "/" + fieldName;
                    }
                    flatFields[fieldName] = field;
                }
            } else {
                var nestedDS = this.getFieldDataSource(field);
                if (path != null) path = (path ? path + "/" : "") + fieldName;
                nestedDS.getFlattenedFields(flatFields, path, pathProperty);
            }
        }
        return flatFields;
    },

    fieldIsComplexType : function (fieldName) {
        var field = this.getField(fieldName);
        if (field == null) return false;
        return (field.type != null && !field.xmlAttribute && this.getSchema(field.type) != null)
            || this.fieldIsAnonDataSource(field);
    },

    fieldIsAnonDataSource : function (field) {
        if (!field.fields) return false;
        var fields = isc.isAn.Array(field.fields) ? field.fields : isc.getValues(field.fields);
        return fields.length > 0 && isc.isAn.Object(fields.get(0));
    },

    getFieldDataSource : function (field, schemaType) {
        // bail if no field passed
        if (!field) return null;

        if (this.fieldIsAnonDataSource(field)) {
            // anonymous datasource field
            if (!field._anonDataSource) {
                // ds hasn't been gen'd before - do so now and cache on the field
                var ds = isc.DataSource.create({ "class": "DataSource", fields: field.fields });
                field._anonDataSource = ds;
            }
            // return the cached ds
            return field._anonDataSource;
        }

        return field.type != null ? this.getSchema(field.type, schemaType) : null;
    },

    // find an XML tag that has the requested type, anywhere in the XML structure that this
    // DataSource and it's child tags represents.  Used for putting together a default
    // recordXPath when calling web services and probably not appropriate for anything else,
    // as the returned tagName isn't a path, just a tagName

    findTagOfType : function (targetType, parentSchema, parentFieldName) {
        //this.logWarn("findTagOfType: searching for targetType: " + targetType);
        // search for immediate fields of the target type
        var fieldNames = this.getFieldNames();
        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = this.getField(fieldName);

            //this.logWarn("findTagOfType: field: " + fieldName + " of type: " + field.type +
            //             (field.type == targetType ? " is a match" :
            //                this.fieldIsComplexType(fieldName) ?
            //                    "(complextype)" : "(simpleType)"));

            if (field.type == targetType) return [this, fieldName, parentSchema, parentFieldName];

            // for complex type fields, check if they in turn have fields of the target type
            if (this.fieldIsComplexType(fieldName)) {
                //this.logWarn("recursing into childTag: " + this.echo(childTag));
                var childDS = this.getFieldDataSource(field),
                    childResult = childDS.findTagOfType(targetType, this, fieldName);
                if (childResult) return childResult;
            }
        }
    },

    // XML Text Content
    // ---------------------------------------------------------------------------------------


    // property to use to store textContent derived from an XML element that also has
    // attributes.  This currently only works for an XML element that has no element children.
    textContentProperty:"xmlTextContent",

    // return the field definition to be used for the text content of an XML element.
    // You can create a field for the textContent (in order to provide type) by just naming a
    // field after the textContentProperty.
    // The schemaTranslator, when encountering <xs:simpleContent>, creates a special field
    // xsdSimpleContent and sets textContentProperty to match.
    getTextContentField : function () {
        return this.getField(this.textContentProperty);
    },

    // in the absence of an explicit textContentField
    hasXMLElementFields : function (textContentProperty) {
        textContentProperty = textContentProperty || this.textContentProperty;
        var fieldNames = this.getFieldNames();
        for (var i = 0; i < fieldNames.length; i++) {
            if (fieldNames[i] == textContentProperty) continue;
            if (this.getField(fieldNames[i]).xmlAttribute) continue;
            return true;
        }
        return false;
    },

    // EditMode
    // ---------------------------------------------------------------------------------------

    //>EditMode get the list of groups defined in the DataSource, which is the order the Groups
    // should appear in the component editor
    getGroups : function () {
        var ds = this;
        while (ds.groups == null && ds.hasSuperDS()) ds = ds.superDS();
        return ds.groups;
    },
    //<EditMode

    // return a field of the type specified by the dataSource ID passed in, searching in order of
    // subclass to superclass
    // includeAllFields: boolean; if true, advanced fields will be included in the search (default
    //   behavior is to omit them)
    // excludedFields: a map of fields to be excluded from the search (keys and values are the
    //   field names)
    _$Defaults : "Defaults",
    _$Properties : "Properties",
    getObjectField : function (targetType, includeAllFields, excludedFields) {

        if (!targetType) return null;

        // search through fields in reverse order to find the best match
        var fields = this.getLocalFields(),
            fieldNames = isc.getKeys(fields).reverse();

        // Find the closest schema for the target, and the class for that schema
        var schemaForType = isc.DataSource.getNearestSchema(targetType),
            classForType = isc.DataSource.getNearestSchemaClass(schemaForType);

        if (excludedFields == null) excludedFields = {};

        // search for the field that has the smallest inheritance distance to the test class
        var nearestDistance = -1,
            nearestMatch = null
        ;

        for (var i = 0; i < fieldNames.length; i++) {
            var fieldName = fieldNames[i],
                field = fields[fieldName],
                distance
            ;

            // HACK: ignore fields intended for properties to configure subobjects
            if (isc.endsWith(fieldName, this._$Properties) ||
                isc.endsWith(fieldName, this._$Defaults)) continue;

            // remember excluded fields, so we can pass the list to our superclass, so that a
            // we won't choose a field in a superclass that was marked "inapplicable" in a
            // subclass
            if (!includeAllFields &&
                (excludedFields[fieldName] ||
                 field.advanced || field.inapplicable || field.hidden ||
                 (field.visibility != null && field.visibility == "internal")))
            {
                excludedFields[fieldName] = fieldName;
                continue;
            }

            // If there is no class for the targetType,
            // then don't try to find a best match based on inheritance distance, just find any
            // matching field
            if (!classForType && field.type == targetType) return fieldName;

            // find the field that is most specific to the targetType by evaluating inheritance
            // distance between the field type and the targetType
            if (classForType && classForType.isA(field.type)) {
                distance = isc.DS.getInheritanceDistance(field.type, targetType);
                //this.logWarn("field " + fieldName +
                //             " has distance from type: " + distance);
                if (nearestMatch == null || distance < nearestDistance) {
                    nearestMatch = fieldName;
                    nearestDistance = distance;
                }
            }
        }

        // match was found in the local fields of this datasource; if it has a superDS, get
        // the best match for the superDS and compare the inheritance distances, returning
        // the match with the smaller distance (if the distances are equal, return the match
        // from the local fields)
        if (nearestMatch != null) {
            if (nearestDistance == 0 || !this.hasSuperDS()) {
                return nearestMatch;
            } else {
                var superDSMatch = this.superDS().getObjectField(
                                        targetType, includeAllFields, excludedFields);
                if (superDSMatch) {
                    var fieldType = this.getField(superDSMatch).type,
                        superDSMatchDistance = isc.DS.getInheritanceDistance(fieldType,
                                                                             targetType);
                }

                return (superDSMatch && (superDSMatchDistance < nearestDistance))
                        ? superDSMatch : nearestMatch;
            }
        // no match was found in the local fields of this datasource, so return the best match
        // found for its superDS, if it has one (recursive search)
        } else if (this.hasSuperDS()) {
            return this.superDS().getObjectField(targetType, includeAllFields, excludedFields);
        }

        return null;
    },


    // Relationship detection
    // --------------------------------------------------------------------------------------------
    // primary keys, foreign keys, within-DS relations like trees, cross-DS relations

    getLocalPrimaryKeyFields : function () {
        if (!this.primaryKeys) {
            this.primaryKeys = {};
            var fields = this.getFields();
            for (var fieldName in fields) {
                var fieldSpec = fields[fieldName];
                if (fieldSpec.primaryKey) {
                    this.primaryKeys[fieldName] = fieldSpec;
                }
            }
        }
        return this.primaryKeys;
    },

    filterPrimaryKeyFields : function (records) {
        var fieldMask = this.getPrimaryKeyFields();
        return isc.applyMask(records, isc.getKeys(fieldMask));
    },

    filterDSFields : function (records) {
        var fieldMask = this.getFields();
        return isc.applyMask(records, isc.getKeys(fieldMask));
    },

    recordHasAllKeys : function (record) {
        var fields = this.getPrimaryKeyFields();
        for (var fieldName in fields) {
            if (record[fieldName] == null) return false;
        }
        return true;
    },

    // Given a foreign record and a foreignDS, returns a map where the keys are the names of
    // the fields in this datasources that are marked as foreignKey into the foreignDS and the
    // values are the foreignRecord values for the fields in the foreignDS that our foreignKeys
    // point into.
    getForeignKeysByRelation : function (foreignRecord, foreignDS) {
        // Figure out the foreignKeys of the ds that will store the binary value
        var foreignKeyFields = this.getForeignKeyFields(foreignDS);
        if (!foreignKeyFields) return {};

        // grab the foreignRecord values for our foreignKeys
        var record = {};
        for (var foreignKeyFieldName in foreignKeyFields) {
            var foreignKeyField = foreignKeyFields[foreignKeyFieldName];
            var foreignFieldName = isc.DataSource.getForeignFieldName(foreignKeyField);

            // don't return an undef or null for a primary key, just omit it
            var value = foreignRecord[foreignFieldName];
            if (value || value === 0) record[foreignKeyFieldName] = value;
        }
        return record;
    },


    //>@method DataSource.getPrimaryKeyFields()
    // Returns this DataSource's +link{DataSourceField.primaryKey,primaryKey} fields as a map
    // of fieldName to field.
    // @return (Record) Javascript object containing all this datasource's primaryKey fields,
    //                  as a map of field name to field
    // @see DataSource.getPrimaryKeyField
    // @see DataSource.getPrimaryKeyFieldNames
    // @visibility external
    //<
    getPrimaryKeyFields : function () {
        if (!this.mergedPrimaryKeys) {
            this.mergedPrimaryKeys = {};

            // if we have a superDS, merge those primaryKeys in first
            if (this.hasSuperDS()) {
                isc.addProperties(this.mergedPrimaryKeys, this.superDS().getPrimaryKeyFields());
            }

            // add local fields
            isc.addProperties(this.mergedPrimaryKeys, this.getLocalPrimaryKeyFields());
        }
        return this.mergedPrimaryKeys;
    },

    // return all foreignKey fields defined on this ds.  If a foreignDS is specified, return
    // only the foreignKeys that point into the foreignDS
    getForeignKeyFields : function (foreignDS) {
        if (isc.isA.DataSource(foreignDS)) foreignDS = foreignDS.ID;

        var fields = this.getFields();
        if (!fields) return null;

        var foreignKeyFields = {};
        for (var fieldName in fields) {
            var field = fields[fieldName];
            if (field.foreignKey) {
                if (foreignDS) {
                    var fieldForeignDS = isc.DataSource.getForeignDSName(field, (foreignDS || this));
                    if (fieldForeignDS != foreignDS) continue;
                }
                foreignKeyFields[field.name] = field;
            }
        }
        return foreignKeyFields;
    },

    getLocalPrimaryKeyFieldNames : function () {
        var localPrimaryKeyFields = this.getLocalPrimaryKeyFields();
        var localFieldNames = [];
        for (var fieldName in localPrimaryKeyFields) {
            localFieldNames.add(fieldName);
        }
        return localFieldNames;
    },

    //>@method DataSource.getPrimaryKeyFieldNames()
    // Returns a list of the names of this DataSource's
    // +link{DataSourceField.primaryKey,primaryKey} fields.
    // @return (Array of String) The list of the names of this datasource's primaryKey fields
    // @see DataSource.getPrimaryKeyFields
    // @visibility external
    //<
    getPrimaryKeyFieldNames : function () {
        return isc.getKeys(this.getPrimaryKeyFields());
    },

    //>@method DataSource.getPrimaryKeyField()
    // Returns a pointer to the primaryKey field for this DataSource.  If this dataSource has
    // a composite primary key (ie, multiple primaryKey fields), returns just the first
    // primaryKey field.
    // @return (DataSourceField) primary key field object
    // @see DataSource.getPrimaryKeyFields
    // @visibility external
    //<
    getPrimaryKeyField : function () {
        var pks = this.getPrimaryKeyFields();
        for (var field in pks) {
            return pks[field];
        }
    },

    //>@method DataSource.getPrimaryKeyFieldName()
    // Returns the primary key fieldName for this DataSource.    If this dataSource has
    // a composite primary key (ie, multiple primaryKey fields), returns just the first
    // primaryKey field name.
    // @return (string) primary key field name
    // @see DataSource.getPrimaryKeyFieldNames
    // @visibility external
    //<
    getPrimaryKeyFieldName : function () {
        return this.getPrimaryKeyFieldNames()[0];
    },


    // Relationships to other DataSources
    // --------------------------------------------------------------------------------------------
    addChildDataSource : function (dataSource) {
        var children = this._childDataSources = (this._childDataSources || []);
        children.add(dataSource);
    },

    getChildDataSources : function () {
        return this._childDataSources;
    },

    getChildDataSource : function (excludeSelf) {
        var childDSs = this.getChildDataSources();
        if (childDSs == null) return null;
        var dataSource;
        for (var i = 0; i < childDSs.length; i++) {
            if (!childDSs[i] || (excludeSelf && childDSs[i] == this)) continue;
            if (!dataSource) {
                dataSource = childDSs[i];
                // don't break - we want to notify the user when this method is returning
                // an ambiguous result
            } else if (dataSource != childDSs[i]) {
                this.logInfo("getChildDatasource(): This DataSource has multiple child " +
                    "DataSources defined making getChildDataSource() ambiguous. Returning the " +
                    "first child dataSource only - call getChildDataSources() to retrieve a " +
                     "complete list.");
                break;
            }
        }
        return dataSource
    },

    // get the default tree relationship declared for this DataSource, or null if no tree
    // relationship can be detected.
    // The returned relationship is of the form
    //            { parentIdField : [fieldName],
    //              idField : [fieldName],
    //              childrenProperty: [fieldName],
    //              parentDS : [dataSource name]}
    // XXX multi-field relations are not currently supported
    //
    // If parentDS is not passed in it will be autodected from either the first foreignKey found on
    // this DS or the one specified via foreignKeyFieldName (second arg).
    //
    // If foreignKeyFieldName is not passed in, we find the first field on this DS that has a
    // foreignKey definition and use that.
    // Note that a valid hierarchical dataSource should have either a foreignKey or
    // childrenProperty field (or both) specified as otherwise we won't be able to link
    // records as parents/children.
    getTreeRelationship : function (parentDS, foreignKeyFieldName) {

        // make sure we have DS instance
        if (isc.isA.String(parentDS)) parentDS = this.getSchema(parentDS);



        // if the name of the foreignKey wasn't passed in, autodetect it by looking for the first
        // field on this ds with a foreignKey pointing at the appropriate dataSource.
        var fields = this.getFields();
        if (foreignKeyFieldName == null) {
            for (var fieldName in fields) {
                var currentField = fields[fieldName];
                if (currentField.foreignKey != null) {
                    // If we were passed no parentDS and no foreignKeyFieldName, always use the
                    // first field with a specified foreignKey
                    if (!parentDS ||
                        (parentDS.getID() == isc.DataSource.getForeignDSName(currentField, this)))
                    {
                        foreignKeyFieldName = fieldName;
                        break;
                    }
                }
            }
        }

        var targetField;
        // if there was no foreignKey property specified on any of the fields, find the first
        // exact field name match between the two datasources.
        if ( foreignKeyFieldName == null && parentDS) {
            foreignKeyFieldName = targetField = isc.getKeys(this.fields).intersect(isc.getKeys(parentDS.fields))[0];
            this.logInfo("no foreign key declaration, guessing tree relationship " +
                         "is on field name: " + foreignKeyFieldName + " which occurs in both DataSources");
        }

        var field;
        if (foreignKeyFieldName) field = fields[foreignKeyFieldName];

        //>DEBUG
        if (field == null) {
            // This is acceptable - it can occur children are served up as arrays directly on nodes.
            this.logDebug("getTreeRelationship(): Unable to find foreignKeyField." +
                          "foreignKeyFieldName specified as:" + foreignKeyFieldName);
        }
        //<DEBUG

        // if parentDS was not passed in and we have a foreignKey field
        // parse it out of the foreignKey property of the field
        if (!parentDS) {
            if (!field) parentDS = this;
            else {
                var parentDSName = isc.DataSource.getForeignDSName(field, this);
                parentDS = this.getSchema(parentDSName);
            }
        }

        if (!targetField) targetField = field ? isc.DataSource.getForeignFieldName(field) : null;

        if (targetField == null) {
            // target field not specified; assume primary key.  Get the primary key of the
            // parent dataSource, limiting to one field
            var idField = parentDS.getPrimaryKeyFieldNames();
            if (isc.isAn.Array(idField)) {
                //>DEBUG
                if (idField.length > 1) {
                    this.logWarn("getTreeRelationship: dataSource '" + parentDS.ID +
                                 "' has multi-field primary key, which is not " +
                                 "supported for tree viewing.  Using field '" +
                                 idField[0] + "' as the only primary key field");
                }
                //<DEBUG
                idField = idField[0];
            }
            targetField = idField;
        }

        var isFolderField;
        var childrenProperty;

        // Suppoert dataSource.childrenField as well as the dataSourceField.childrenProperty
        // boolean
        if (this.childrenField) childrenProperty = this.childrenField;

        for (fieldName in fields) {
            var field = fields[fieldName];
            // autodetect a field marked as the 'isFolderProperty' and 'childrenProperty' and
            // make that available via the relationship
            if (field.isFolderProperty) isFolderField = fieldName;
            if (field.childrenProperty) childrenProperty = fieldName;

            // By default, set field.multiple to true for the children field.
            // This expects XML type data to have explicit an array of child elements under
            // the childrenProperty node - like this:
            //   <item name=.../>
            //      <children>
            //          <item name=.../>
            //          <item name=.../>
            //      </children>
            //   </item>
            // However - if field.multiple is set to false, dont override it.
            // When this property is set to false, the server can still deliver an array of
            // elements by simply having a number of same-named child nodes - like this:
            //  <item name=.../>
            //      <child name=.../>
            //      <child name=.../>
            //  </item>
            if (childrenProperty == fieldName && (field.multiple == null)) {
                field.multiple = true;
            }
        }

        var relationship = {
            childDS : this,
            parentDS : parentDS,
            isFolderProperty: isFolderField
        }
        // the field on this DataSource with the foreignKey declaration
        // points to "idField" on the parent DataSource
        if (foreignKeyFieldName) {
            relationship.parentIdField = foreignKeyFieldName;
            relationship.idField = targetField;
        }
        if (childrenProperty) relationship.childrenProperty = childrenProperty;

        // If both foreignKey field name and childrenProperty are null there's no way we can
        // serve up children meaningfully:
        // This is not necessarily an error - this method may be called when there is no
        // foreignKey relationship -- EG databound treeGrid drop
        //>DEBUG
        if (childrenProperty == null && foreignKeyFieldName == null) {
            this.logInfo("getTreeRelationship(): No specified foreignKeyField or childrenProperty.")
        }
        //<DEBUG

        // determine the rootValue.  This applies only to within-DataSource trees; for
        // cross-DataSource trees, in effect all records of the parentDS are roots.
        // rootValue defaults to null.
        if (parentDS == this) {
            var rootValue = foreignKeyFieldName ? this.getField(foreignKeyFieldName).rootValue : null;

            if (rootValue == null) relationship.rootValue = null;
            else relationship.rootValue = rootValue;
        }

        // NOTE: there may be more than one tree relationship.  For now we're just
        // auto-detecting the first.

        return relationship;
    },

    // DataBinding support
    // --------------------------------------------------------------------------------------------

    _$name:"name",
    combineFieldOrders : function (parentFields, localFields, shouldUseFieldFunc) {
        // all the fields in the DataSource that would normally be shown will be shown in the
        // order they occur in the DataSource.  The specified fields act as overrides.
        // NOTE: in this mode, if you specify any non-DataSource fields, they appear right
        // after the last DataSource field you added.  In this way you can add extra
        // fields, or items like spacers, at specific positions with respect to DataSource
        // fields.

        // create a new array to hold the combined field specifications
        var combinedFields = [];

        // add any non-DS fields before the first DS field
        this._addNonDSFields(localFields, 0, parentFields, combinedFields, shouldUseFieldFunc);

        // go through all dataSource fields
        for (var fieldName in parentFields) {
            var parentField = parentFields[fieldName],
                // look for a localField with matching name (this is slow but not time
                // critical)
                localFieldIndex =
                    localFields.findIndex(this._$name, fieldName);

            if (localFieldIndex != -1) {
                // combine the specified field and the reference dsField
                var localField = localFields[localFieldIndex],

                    combinedField = this.combineFieldData(localField);

                // check if this is a field this component should use.

                if (shouldUseFieldFunc == null ||
                    shouldUseFieldFunc(combinedField, this, true)) combinedFields.add(combinedField);

                // if there are any non-dataSource fields specified after this dsField, add them
                // here
                this._addNonDSFields(localFields, localFieldIndex + 1,
                                     parentFields, combinedFields, shouldUseFieldFunc);
            } else {
                // no specified field data for this dsField, just clone the dsField
                if (shouldUseFieldFunc == null ||
                    shouldUseFieldFunc(parentField, this)) {
                    var combinedField = isc.addProperties({}, parentField);
                    // hidden property - if dataSourceField.hidden is set, we typically
                    // skip the field altogether
                    // however, if "showHiddenFields" is true on the component we want
                    // the field to show - wiping the "hidden" flag is required to ensure
                    // the "showIf" method isn't auto-set to return false.
                    if (combinedField.hidden) delete combinedField.hidden;
                    combinedFields.add(combinedField);
                }
            }
        }
        return combinedFields;
    },

    // starting at position, add in any fields that don't refer to DataSource fields
    _addNonDSFields : function (localFields, position, parentFields, combinedFields,
                                shouldUseFieldFunc)
    {
        for (var i = position; i < localFields.length; i++) {
            var field = localFields[i];
            // field refers to a DataSource field; stop
            if (field.name != null && parentFields[field.name] != null) return;

            if (shouldUseFieldFunc == null ||
                !shouldUseFieldFunc(field, this, true)) continue;

            // field doesn't refer to a DataSource field; add it and continue
            // NOTE: type defaults are auto-applied to DS fields and combined fields
            isc.SimpleType.addTypeDefaults(field);
            combinedFields.add(field);
        }
    },

    //> @method combineFieldData()  ([IA])
    //  Internal method to combine data from a field defined on the dataSource with a field object
    //  passed in.
    // @param   field   (object)    field to update with DS field defaults.
    // @param   targetFieldName   (string)    name of the DS field to copy properties from when
    //    the "field" object passed in the first param is not in the DS
    // @return  (object)            field with its data combined with the DS data
    // @visibility internal
    //<

    // isDataSource attribute is used to distinguish between combining attributes for a
    // dataBoundComponent field definition vs dataSource inheritance. Some properties
    // are treated differently, such as "hidden"
    combineFieldData : function (localField, targetFieldName, isDataSource, propertiesAttr) {
        var parentField;
        if (isc.isAn.Object(targetFieldName)) parentField = targetFieldName;
        else parentField = this.getField(targetFieldName || localField.name);
        return isc.DataSource.combineFieldData(localField, parentField, isDataSource, propertiesAttr);
    },

    // SimpleType handling: local types and type defaults
    // ----------------------------------------------------------------------------------------

    // add validators and other defaults that can be declared as part of a type to the field
    // definition.  NOTE: DataSources do this once on init.  fields that don't come from a
    // DataSource do this during DataBinding (see Canvas.bindtoDataSource)
    _addTypeDefaults : function (fields) {

        if (fields == null) fields = this.fields;

        // add validators to each field, based on the specified type for the field
        for (var fieldName in fields) {
            var field = fields[fieldName];

            if (field && field.required == null && field.xmlRequired != null &&
                field.xmlNonEmpty != null)
            {
                field.required = field.xmlRequired && field.xmlNonEmpty
            }

            // if a field is marked as the childrenProperty
            if (field && (field.childrenProperty || field.name == this.childrenField)) {

                // for correct recursive transform of either XML or JSON, it needs to have the
                // type of this DataSource.
                if (!field.type) field.type = this.ID;
            }

            // Cater for the specific case of a field that overrides a super-field but doesn't
            // reiterate the field type.  We do this to make sure we get the correct type-derived
            // validators, because we haven't yet been through the full inheritance process
            if (!field.type && this.hasSuperDS()) {
                if (this.superDS().fields && this.superDS().fields[fieldName]) {
                    field.type = this.superDS().fields[fieldName].type;
                }
            }

            isc.SimpleType.addTypeDefaults(field, this);
            this._addFieldValidators(field);
        }
    },

    // Add validators that replace basic field properties (ex. required)
    _addFieldValidators : function (field) {

        var requiredValidator = {type: "required"};

        if (field.required) {
            var validator = isc.addProperties({}, requiredValidator),
                message = field.requiredMessage || this.requiredMessage
            ;
            if (message != null) validator.errorMessage = message;

            // Add validator to field
            if (!field.validators) {
                field.validators = [validator];
            } else {
                if (!isc.isAn.Array(field.validators)) {
                    field.validators = [field.validators];
                }
                // if the field is using the shared, default validators for the type,
                // make a copy before modifying
                if (field.validators._typeValidators) {
                    field.validators = field.validators.duplicate();
                }
                field.validators.add(validator);
            }
        }
    },

    _autoDeriveTitles : function () {
        if (!this.autoDeriveTitles) return;
        for (var fieldName in this.fields) {
            var field = this.fields[fieldName];
            if (field.title != null) continue;

            field.title = this.getAutoTitle(fieldName);
            field._titleAutoDerived = true;
        }
    },

    //> @method dataSource.getAutoTitle()
    // Return a reasonable user-visible title given a fieldName.  Called when
    // +link{autoDeriveTitles} is true and by default, calls the class method
    // +link{classMethod:DataSource.getAutoTitle()}.  Override to provide a different policy
    // for auto-deriving titles for a particular DataSource or subclass of DataSource.
    //
    // @param identifier (String) identifier for which a title is desired.
    // @return (String) auto-derived title
    //
    // @group title
    // @visibility external
    //<
    getAutoTitle : function (identifier) {
        return isc.DataSource.getAutoTitle(identifier);
    },

    // get the simple type object for the specified typeName, if any, taking into account types
    // declared just on this DataSource
    _$type: "type",
    getType : function (typeName) {
        if (this.schemaNamespace) {
            var schemaSet = isc.SchemaSet.get(this.schemaNamespace),
                type = schemaSet.getSimpleType(typeName);
            if (type) return type;
        }

        // search for globally defined type first
        var type = isc.SimpleType.getType(typeName);
        if (type != null) return type;
        // then search for locally defined types
        if (this.types && this.types[typeName]) return this.types[typeName];
        return null;
    },

    //>LocalDS mode (serverless DataSource)
    // -----------------------------------------------------------------------------------------


    firstCacheAllDataRequest : function (dsRequest) {

        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("firstCacheAllDataRequest: refreshing cache", "cacheAllData");
        }
        // defer all other operations against this datasource until this fetch
        // completes, starting with this one
        this._deferredRequests = [dsRequest];

        // In cacheAllData mode, the dataSource becomes clientOnly wrt fetches until
        // an explicit refresh of the cache is issued.
        // As such standard 'transformRequest' / 'transformResponse' logic is likely inapplicable.
        // We can't simply throw away transformRequest - it will still need to be called for
        // non fetch operations and for fetches if the cache is refreshed. Instead
        // wrap it in a function that no-op's if the request is clientOnly
        if (this._insulatedTransforms == null) {
            // Rename original transform methods
            // Note: If these are being observed, be sure to copy across the underlying
            // method, not the observation notification method
            var transformRequest = this.transformRequest,
                transformResponse = this.transformResponse,
                observers = this._observers;
            if (observers) {
                if (observers.transformRequest) {
                    transformRequest = this[isc._obsPrefix + "transformRequest"]
                }
                if (observers.transformResponse) {
                    transformResponse = this[isc._obsPrefix + "transformResponse"]
                }
            }
            this.transformServerRequest = transformRequest;
            this.transformServerResponse = transformResponse;

            // Replace with functions that call the original logic if the request/response isn't
            // marked as clientOnly
            // Note that addMethods will preserve observation.
            // dataProtocol: clientCustom is an odd case, we need to call the original
            // transformRequest (now called transformServerRequest) or we'll
            // never receive a response at all.
            this.addMethods({
                _isServerRequest : function (dsRequest) {

                    return dsRequest.cachingAllData ||
                        (this.cacheAcrossOperationIds &&
                            (dsRequest.operationType && dsRequest.operationType != "fetch"))
                        ||
                        (this.cacheAcrossOperationIds === false &&
                            this.cacheAllOperationId !=
                                (dsRequest.operationId || dsRequest.operation)
                        )
                    ;
                },
                transformRequest : function (dsRequest) {
                    if (!this._isServerRequest(dsRequest)) return dsRequest;
                    return this.transformServerRequest(dsRequest);
                },
                transformResponse : function (dsResponse,dsRequest,data) {
                    if (!this._isServerRequest(dsRequest)) {
                        var cacheAllDataTime = this._autoCacheAllData_timestamp,
                            sentTime = dsRequest._sentTime;

                        if (!cacheAllDataTime || !sentTime || sentTime >= cacheAllDataTime)
                            return dsResponse;
                    }
                    return this.transformServerResponse(dsResponse,dsRequest,data);
                }
            });

            // Set the flag to indicate we've set up these 'insulated' methods

            this._insulatedTransforms = true;
        }

        this.cacheResultSet = isc.ResultSet.create({
            dataSource: this,
            fetchMode: "local",
            fetchOperation: this.cacheAcrossOperationIds? dsRequest.operationId: this.cacheAllOperationId,
            allRows: this.cacheData ? this.cacheData : null,
            cachingAllData: true,
            componentId : "(cacheAllData fetch)",
            componentContext : "(from: " + dsRequest.componentId +
                    (dsRequest.componentContext ? " &lt;" + dsRequest.componentContext + "&gt;)"
                                                : ")"),
            dataArrived : function (startRow, endRow) {
                if (this.logIsInfoEnabled("cacheAllData")) {
                    this.logInfo("cacheAllData - cacheResultSet.dataArrived: startRow/endRow: "+startRow+"/"+endRow);
                }
                if (this.lengthIsKnown()) {
                    var ds = this.getDataSource();
                    if (ds.cacheResultSet == null) return;
                    ds.cacheLastFetchTime = new Date().getTime();
                    // if both cacheAllData and clientOnly are set, we do the initial fetch
                    // here according to cacheAllData, but we then switch to clientOnly by
                    // setting ds.testData/cacheData
                    if (ds.clientOnly) ds.testData = ds.cacheData = this.getAllRows();

                    ds.processDeferredRequests();
                }
            }
        });

        if (!this.cacheData) {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("firstCacheAllDataRequest: issuing fetch", "cacheAllData");
            }
            this.cacheResultSet.get(0);
            return true; // request has been deferred, return true to halt further processing
        } else {
            if (this.logIsInfoEnabled("cacheAllData")) {
                this.logInfo("firstCacheAllDataRequest: updating last fetch time", "cacheAllData");
            }
            this.cacheLastFetchTime = new Date().getTime();
            if (this.clientOnly) this.testData = this.cacheData;
            this.processDeferredRequests();
            // request was deferred and has just been processed - return true to halt further
            // processing
            return true;
        }

    },

    //> @method dataSource.getCacheData()
    // Returns the complete set of data cached by this dataSource. Note that this may
    // have been supplied via +link{dataSource.cacheData}, or may have been fetched
    // from the server for dataSources with +link{dataSource.cacheAllData} set to true.
    // @return (Array of Records) entire cached set of data
    // @visibility external
    //<
    getCacheData : function () {
        // cacheAllData RS's use "cacheResultSet" rather than just cacheData.
        if (this.cacheResultSet != null) {
            if (this.cacheResultSet.allRows) return this.cacheResultSet.allRows;
        }
        if (this.cacheData != null) {
            return this.cacheData;
        }
        if (this.testData != null) {
            return this.testData;
        }
    },

    // Given a dsRequest - if this is a clientOnly / cacheAllData dataSource, do we currently
    // have a cache to perform client-side operations against?
    // Returns false if the cache exists allowing the calling code in sendDSRequest to continue
    // and perform a client-side fetch against that test data.
    // Otherwise kicks off the one-time fetch if necessary, adds the request to the
    // deferredRequests queue and returns true indicating that the calling code should stop
    // processing and wait for the one time fetch to return.
    fetchingClientOnlyData : function (dsRequest)
    {
        if (dsRequest.cachingAllData) {
            //delete dsRequest.cachingAllData;
            return false;
        }

        // when downloadResult is true, we bypass the cache and hit the server
        // even if dataSource.cacheAllData is true
        if (dsRequest.downloadResult) {
            return false;
        }

        var useTestDataFetch = (this.useTestDataFetch == null ?
                                this.shouldUseTestDataFetch() :
                                this.useTestDataFetch);

        if (this.logIsInfoEnabled("cacheAllData")) {
            this.logInfo("fetchingClientOnlyData: useTestDataFetch is "+useTestDataFetch, "cacheAllData");
        }

        // mark the request client-only: this causes the RPCManager to avoid sending
        // this RPC to the server.  Note that, if clientOnly requests were not allowed to be
        // involved in transactions, we could just fire the passed-in callback immediately with
        // the client-only dsResult
        if (this.clientOnly) {
            dsRequest.clientOnly = true;
            if (this.testData && !this.cacheData) this.cacheData = this.testData;
            else if (this.cacheData && !this.testData) this.testData = this.cacheData;
        }

        // if we're deferring requests, add request to queue and return
        if (this._deferredRequests) {
            this._deferredRequests.add(dsRequest);
            return true; // request has been deferred, return true to halt further processing
        }

        var cacheNeedsRefresh = this.cacheNeedsRefresh();

        if (useTestDataFetch == false && this.clientOnly && this.hasTestData())
            useTestDataFetch = true;
        if (!useTestDataFetch && ((this.cacheAllData && cacheNeedsRefresh) ||
                (this.clientOnly && !this.testData && this.hasTestData())
            ))
        {
            // we're not using a testDataFetch and either we're in cacheAllData mode and the
            // cache needs refreshing, or we're in clientOnly mode and the cache hasn't been
            // fetched yet
            return this.firstCacheAllDataRequest(dsRequest);
        } else {
            // if we're in clientOnly or cacheAllData mode and there's no testData, but we do
            // have a dataURL or testFileName, do a one-time load, then re-run current
            // operation against the resulting testData.  If we're in cacheAllData mode,
            // initialize the cacheResultSet with the resulting testData - further client-side
            // fetching will take place against that resultSet.
            if (this.clientOnly && !this.testData && (this.testFileName || this.dataURL) ||
                (this.cacheAllData && cacheNeedsRefresh)){

                if (this.logIsInfoEnabled("cacheAllData")) {
                    this.logInfo("fetchingClientOnlyData: issuing oneTimeDS fetch", "cacheAllData");
                }

                // defer all other operations against this datasource until this fetch
                // completes, starting with this one
                this._deferredRequests = [dsRequest];

                // perform a one-time fetch by creating a datasource that picks up the fields,
                // but not the operationBindings or other properties.  This way we don't have
                // to muck with this DS and try to restore settings

                // if only testFileName is set, use that as the dataURL
                var dataURL = this.dataURL || this.testFileName;

                // if dataFormat is the default iscServer, set it based on the file extension
                // of the dataURL, since it doesn't really make sense to have a clientOnly
                // dataSource of type iscServer
                var dataFormat = this.getDataFormat(dsRequest);
                if (dataFormat == "iscServer") dataFormat = dataURL.match(/\.xml$/i) ? "xml" : "json";

                var operationBinding = this.getOperationBinding(dsRequest);

                // Note: if someone's observing transformRequest/response, ensure we copy the
                // original methods across - otherwise we'll get JS errors since we're not also
                // copying the observers across to this one-time DS.

                var transformRequest = this.transformRequest,
                    transformResponse = this.transformResponse,
                    observers = this._observers;
                if (observers) {
                    if (observers.transformRequest) {
                        transformRequest = this[isc._obsPrefix + "transformRequest"]
                    }
                    if (observers.transformResponse) {
                        transformResponse = this[isc._obsPrefix + "transformResponse"]
                    }
                }
                // check for recordName on the opBinding, then on this DS - failing
                // that, if this DS inherits from another DS, use the ID of the parent before
                // the child to work out the recordName
                var localRecordName = operationBinding.recordName || this.recordName ||
                        (this.inheritsFrom ? (
                            isc.isA.String(this.inheritsFrom) ?
                                this.inheritsFrom : this.inheritsFrom.ID)
                        : this.ID);
                var oneTimeDS = isc[this.Class].create({
                    ID: this.ID+"_oneTime",
                    inheritsFrom: this.ID,
                    dataURL: dataURL,
                    dataFormat: dataFormat,
                    recordXPath: this.recordXPath,
                    // use our transformRequest/response on the one-time DS to canonicalize data to
                    // our internal testData format.
                    _isServerRequest: this._isServerRequest,
                    transformRequest: transformRequest,
                    transformResponse: transformResponse,
                    recordName: localRecordName,
                    showPrompt: this.showPrompt
                });

                this.logInfo("clientOnly datasource performing one-time " + dataFormat +
                             " fetch via: " + dataURL);

                // reset our transformRequest/response to default DataSource versions b/c we'll
                // be working against a local testData from now on and transforms no longer apply.
                this.addProperties({
                    transformRequest: isc.DataSource.getInstanceProperty("transformRequest"),
                    transformResponse: isc.DataSource.getInstanceProperty("transformResponse")
                });

                var ds = this;
                // note: don't pass criteria to the one-time fetch as we want to fetch all the
                // records for our cache, we'll apply the criteria in a follow-up local
                // operation against the cache

                if (this.cacheAllData) {
                    oneTimeDS.cacheAllData = false;
                }

                oneTimeDS.sendDSRequest({
                    operationType : "fetch",
                    willHandleError:true,
                    // copy over the componentId and componentContext so it's easy to see what
                    // request initiated this one
                    componentId:dsRequest.componentId,
                    componentContext:dsRequest.componentContext,
                    callback : function (dsResponse, data) {
                        var cacheRows;
                        if (dsResponse.status != isc.DSResponse.STATUS_SUCCESS) {
                            ds.logWarn("one-time fetch failed with status: " + dsResponse.status +
                                       " and messsage: " + (data ? data : "N/A") +
                                       ".  Initializing an empty Array as testData.");
                            cacheRows = [];
                        } else {
                            ds.logInfo("One-time fetch complete: "+ (data ? data.length : "null") + " records");

                            cacheRows = ds.initializeSequenceFields(data);
                        }

                        if (ds.cacheAllData) {
                            ds.cacheLastFetchTime = new Date().getTime();
                            ds.cacheResultSet = isc.ResultSet.create({
                                dataSource: ds.ID,
                                fetchMode: "local",
                                allRows: cacheRows
                            });
                            ds.cacheLastFetchTime = new Date().getTime();
                        }

                        if (ds.clientOnly) {
                            ds.cacheData = ds.testData = cacheRows;
                        }

                        ds.processDeferredRequests();
                        oneTimeDS.destroy();
                    }
                });

                return true; // request has been deferred, return true to halt further processing
            }
        }
    },

    // NOTE: These methods are overridden in RestDataSource
    shouldUseTestDataFetch : function () {
        return this.clientOnly == true &&
               this.cacheAllData != true &&
               (this.dataURL != null || this.testFileName != null);
    },
    hasTestData : function () {
        return this.dataURL != null || this.testFileName != null;
    },

    //> @method dataSource.getClientOnlyResponse()
    // Return a "spoofed" response for a +link{dataSource.clientOnly,clientOnly} or +link{cacheAllData} DataSource.
    // <P>
    // The default implementation will use +link{dataSource.cacheData} to provide an appropriate
    // response, by using +link{applyFilter,client-side filtering} for a "fetch" request, and
    // by modifying the <code>testData</code> for other requests.
    // <P>
    // Override this method to provide simulations of other server-side behavior, such as
    // modifying other records, or to implement <b>synchronous</b> client-side data providers
    // (such as Google Gears).  For <b>asynchronous</b> third-party data providers, such as
    // GWT-RPC, HTML5 sockets, or bridges to plug-in based protocols (Java, Flash,
    // Silverlight..), use +link{DSProtocol,dataProtocol:"clientCustom"} instead.
    // <P>
    // Overriding this method is also a means of detecting that a normal DataSource (not
    // clientOnly) would be contacting the server.
    //
    // @param request (DSRequest) DataSource request to respond to
    // @param serverData (Array of Record) for cacheAllData DataSources, the data from the local cache
    // @return (DSResponse)
    // @visibility external
    //<
    getClientOnlyResponse : function (request, serverData) {
        //!OBFUSCATEOK
        // initialize the spoofed dataset
        serverData = serverData || this.testData;

        // if we have serverData but no testData, it's a cacheAllData result - if we're also in
        // clientOnly mode, we need to set up the clientOnly cache - set both cacheData and the
        // now deprecated testData to the same array
        if (serverData && !this.testData && this.clientOnly)
            this.cacheData = this.testData = serverData;

        if (!serverData || isc.isA.String(serverData)) {
            if (isc.isA.String(serverData)) {
                // ID or expression
                //>DEBUG
                this.logInfo(this.ID + " datasource: using testData property as data");
                //<DEBUG
                this.cacheData = this.testData = isc.eval(serverData);
            } else if (window[this.ID + "TestData"]) {
                // dataset loaded in page under canonical name
                //>DEBUG
                this.logInfo(this.ID + " datasource: using " + this.ID + "TestData object as data");
                //<DEBUG
                this.cacheData = this.testData = window[this.ID + "TestData"];
            } else {
                // initialize to empty list
                //>DEBUG
                this.logInfo(this.ID + " datasource: testData property and " + this.ID +
                             "TestData object not found, using empty list as data");
                //<DEBUG
                this.cacheData = this.testData = [];
            }
            serverData = this.testData;
        }

        var operationType = request.operationType,
            response = {
                status:0
            };

        switch (operationType) {
            case "fetch":
            case "select":
            case "filter":
                var criteria = request.data;
                // support multiple where clause format (don't think this is actually valid any
                // more)
                if (isc.isAn.Array(criteria)) criteria = criteria[0];
                var filteredData = this.applyFilter(serverData, criteria, request),
                    resultData = filteredData;

                if (request.startRow != null) {
                    // simulate paging
                    var startRow = request.startRow,
                        endRow = request.endRow,
                        totalRows = filteredData.length;

                    // server handles sort, so sort the data by the requested direction

                    var sortProperty = isc.shallowClone(request.sortBy);
                    if (sortProperty) {
                        if (!isc.isAn.Array(sortProperty)) sortProperty = [sortProperty];

                        if (isc.isAn.Object(sortProperty[0])) {
                            // if this is an array or SortSpecifiers, map them to sortBy strings
                            sortProperty = isc.DS.getSortBy(sortProperty)
                        }

                        var sortDirs = [];

                        for (var i = 0; i < sortProperty.length; i++) {
                            var sortDirection = true;
                            if (sortProperty[i].startsWith("-")) {
                                sortProperty[i] = sortProperty[i].substring(1);
                                sortDirection = false;
                            }
                            sortDirs[i] = sortDirection;
                        }
                        filteredData.sortByProperties(sortProperty, sortDirs);
                    }

                    // cap the endRow at one after last valid index (it's exclusive)
                    endRow = Math.min(endRow, totalRows);
                    // slice out from startRow to endRow (note: slice is non-inclusive at the end)
                    resultData = filteredData.slice(startRow, endRow);
                    response.startRow = startRow;
                    response.endRow = endRow;
                    response.totalRows = totalRows;
                }

                // shallow copy the results.  This allows test or example cases where the
                // "server data set" is changing independently of the client results
                if (this.copyLocalResults) {
                    var isSGWT = isc.Browser.isSGWT;
                    for (var i = 0; i < resultData.length; i++) {
                        if (this.deepCopyLocalResults) {
                            resultData[i] = isc.clone(resultData[i]);
                        } else {
                            resultData[i] = isc.addProperties({}, resultData[i]);
                        }
                        // Clear out SGWT's __ref/__module pointers so that if the cacheData is created in
                        // SGWT, Record.getOrCreateRef() won't return the same Java Record object for the
                        // copied result record.
                        if (isSGWT) {
                            resultData[i][isc.gwtRef] = null;
                            resultData[i][isc.gwtModule] = null;
                        }
                    }
                }

                response.data = resultData;
                break;
            // NOTE: for modification operations, we always return a copy, "copyLocalResults"
            // doesn't need to be set.
            case "remove":
            case "delete": // old name
                if (this.isMissingPrimaryKeys(request.data)) {
                    response.data = "clientOnly remove operation failed for DataSource " +
                                        this.ID + ": missing primaryKey values(s) " +
                                        this.getMissingPrimaryKeys(request.data);
                    response.data = "clientOnly remove operation failed: missing primaryKey " +
                                    "value(s): " + this.getMissingPrimaryKeys(request.data);
                    response.status = -1;
                } else {
                    var serverRecordIndex = this.findByKeys(request.data, serverData);
                    if (serverRecordIndex == -1) {
                        this.logWarn("clientOnly remove operation: Unable to find record matching criteria:"
                                    + this.echo(request.data));
                        response.data = "clientOnly remove operation failed: unable to find matching " +
                                        "record"
                        response.status = -1;
                    } else {
                        serverData.removeAt(serverRecordIndex);
                        response.data = isc.addProperties({}, request.data);
                    }
                }
                break;
            case "add":
            case "insert": // old name
                // Generate values for sequence fields at this point
                var serverRecord = isc.addProperties({}, request.data);

                if (serverRecord.__ref) {
                    delete serverRecord.__ref;
                    delete serverRecord.__module;
                }
                serverRecord = this.applySequenceFields(serverRecord);

                // Check for duplicate key
                var serverRecordIndex = this.findByKeys(serverRecord, serverData);
                if (serverRecordIndex != -1) {
                    this.logWarn("clientOnly add operation: Duplicate key: " +
                                        isc.echoAll(serverRecord));
                    response.data = "clientOnly add operation failed for DataSource " +
                                        this.ID + ": Duplicate key in record " +
                                        isc.echoAll(serverRecord);
                    response.status = -1;
                } else {
                    // make both the saved data and returned data a distinct copy
                    // from the passed data
                    serverData.add(serverRecord);
                    response.data = isc.addProperties({}, serverRecord);
                }
                break;
            case "replace":
            case "update":
                if (this.isMissingPrimaryKeys(request.data)) {
                    this.logWarn("clientOnly update operation: Missing primaryKey values: " +
                                        this.getMissingPrimaryKeys(request.data));
                    response.data = "clientOnly update operation failed for DataSource " +
                                        this.ID + ": missing primaryKey values(s) " +
                                        this.getMissingPrimaryKeys(request.data);
                    response.status = -1;
                } else {
                    // find the server record
                    var serverRecordIndex = this.findByKeys(request.data, serverData);
                    if (serverRecordIndex == -1) {
                        this.logWarn("clientOnly update operation: Unable to find record matching criteria:"
                                    + this.echo(request.data));
                        response.data = "clientOnly update operation failed: unable to find matching " +
                                        "record. Did you supply all primaryKeys?"
                        response.status = -1;
                    } else {
                        var serverRecord = serverData[serverRecordIndex];
                        // update the server record in place
                        for (var key in request.data) {

                            if (key == "__ref" || key == "__module") continue;

                            var field = this.getField(key);
                            // Note we are not passing in the 'field' object as a parameter.
                            // This is because if the field is of a simpleType with
                            // a get/update atomic value pair we do not want to run through
                            // the 'updateAtomicValue' method -- the record passed in should
                            // already have the field value in opaque format.
                            isc.Canvas._saveFieldValue(key, null, request.data[key], serverRecord, null, true);
                        }
                        // return a copy
                        response.data = isc.addProperties({}, serverRecord);
                    }
                }
                break;
            case "validate":
            default:
                break;
        }

        this._trackClientOnlyChanges(response, operationType);

        return response;
    },

    _trackClientOnlyChanges : function(response, operationType) {

        // we will store the changes in separate arrays, according to operationType
        // storing the response data, and the arrays will be indexed by primary key value
        var pk = this.getPrimaryKeyFieldName();

        switch(operationType) {
            case 'add':
                if (!this._addedRecords) {
                    this._addedRecords = new Array();
                }
                this._addedRecords[response.data[pk]] = response.data;
                break;

            case 'update':
                if (!this._updatedRecords) {
                    this._updatedRecords = new Array();
                }

                // if we're trying to update a previously added record, then update that
                // record's values instead of recording an update
                if (this._addedRecords && this._addedRecords[response.data[pk]]) {
                    this._addedRecords[response.data[pk]] = response.data;
                } else {
                    this._updatedRecords[response.data[pk]] = response.data;
                }
                break;

            case 'remove':
                if (!this._removedRecords) {
                    this._removedRecords = new Array();
                }

                // if a newly added record is removed, then should not be added at all
                // so we remove it from the list of added records
                if (this._addedRecords && this._addedRecords[response.data[pk]]) {
                    this._addedRecords[response.data[pk]] = null;
                }

                // if an updated record is removed, then should not be updated
                // so ve remove it from the list of updated records
                if (this._updatedRecords && this._updatedRecords[response.data[pk]]) {
                    this._updatedRecords[response.data[pk]] = null;
                }
                this._removedRecords[response.data[pk]] = response.data;
        }
    },

    //> @method dataSource.getChanges()
    //
    // Return the changes made on a client-only DataSource in a form of DSRequest array
    // which can be used to update the records with a non-client only DataSource.
    // <p>
    // The list is built according to following rules:
    // <ul>
    //  <li>Updates to the same record are collapsed into a single update.</li>
    //  <li>Deletions of a record cancels any previous updates to the record.</li>
    //  <li>If a new record is created and then updated, this results in one "add" operation using the latest values.</li>
    //  <li>If a new record is removed, there will be no requests for it.</li>
    // </ul>
    //
    // @return (Array of DSRequest) request objects containing the changes on this client-only DataSource.
    //<
    getChanges : function() {

        var result = new Array();
        var pk = this.getPrimaryKeyFieldName();

        if (this._addedRecords) {
            for(var id in this._addedRecords) {
                if (this._addedRecords[id] != null && isc.isA.Object(this._addedRecords[id])) {
                this._addedRecords[id][pk] = null;
                    var req = {
                        data: this._addedRecords[id],
                        operationType:'add'
                    }
                    result.push(req);
                }
            }
        }

        if (this._updatedRecords) {
            for(var id in this._updatedRecords) {
                if (this._updatedRecords[id] != null && isc.isA.Object(this._updatedRecords[id])) {
                    var req = {
                        data : this._updatedRecords[id],
                        operationType:'update'
                    }
                    result.push(req);
                }
            }
        }

        if (this._removedRecords){
            for(var id in this._removedRecords) {
                if (this._removedRecords[id] != null && isc.isA.Object(this._removedRecords[id])) {
                    var req = {
                        data : this._removedRecords[id],
                        operationType:'remove'
                    }
                    result.push(req);
                }
            }
        }

        // remove the rows from the arrays used to track them, so at next call won't be reported again
        if (this._addedRecords) {this._addedRecords.splice(0, this._addedRecords.length);}
        if (this._updatedRecords) {this._updatedRecords.splice(0, this._updatedRecords.length);}
        if (this._removedRecords) {this._removedRecords.splice(0, this._removedRecords.length);}

        return result;
    },

    // Generating sequence values in client-only mode
    getNextSequenceValue : function (field) {
        var data = this.testData,
            value = 0;
        for (var i = 0; i < data.length; i++) {
            var fieldValue = data[i][field.name];
            if (fieldValue != null && fieldValue > value) value = fieldValue;
        }
        // So this returns the largest of the stored values, +1
        return value+1;
    },

    applySequenceFields : function (record) {

        if (this.contactsServer()) {

            return record;
        }

        var fields = this.getFields();
        for (var fieldName in fields) {
            var field = fields[fieldName];
            // also auto-gen primaryKeys that may not be of type="sequence"

            if ((field.type == "sequence" || field.primaryKey) && record[fieldName] == null) {
                var type = isc.SimpleType.getType(field.type);
                while (type.inheritsFrom) type = isc.SimpleType.getType(type.inheritsFrom);
                if (type == "date" || type == "time") {
                    this.logWarn("clientOnly dataSource encountered a primaryKey of date or " +
                                 "time type for which no value was supplied for an 'add' " +
                                 "request.  Defaulting to the epoch (midnight on 1 Jan 1970)");
                    record[fieldName] = new Date(0);
                } else {
                    record[fieldName] = this.getNextSequenceValue(field);
                    if (field.type != "sequence") {
                        this.logWarn("clientOnly dataSource found a missing value for primaryKey " +
                                     "field '" + fieldName + "' during an add request. Derived " +
                                     "value " + record[fieldName] + " from sequence processing");
                    }
                }
            }
        }
        return record;
    },


    // generate primary keys in test data if not present (as frequently happens
    // where the storage layer is intended to auto-assign the primaryKey values
    // - e.g. Database sequences)
    //
    // This method assumes a potentially large number of records, where calling
    // applySequenceFields() per record would not be appropriate.  Part of the optimization
    // here is that we don't bother supporting gappy records - in other words, the primaryKeys
    // are either present or they're not - we don't attempt to intelligently fill them in as that
    // can be too slow
    initializeSequenceFields : function (records) {
        if (!isc.isAn.Array(records)) return;

        var fields = this.getFields();
        var sequenceFields = [];
        for (var fieldName in fields) {
            if (fields[fieldName].type == "sequence" || fields[fieldName].primaryKey) sequenceFields.add(fieldName);
        }

        for (var i = 0; i < records.length; i++) {
            for (var j = 0; j < sequenceFields.length; j++) {
                var fieldName = sequenceFields[j];
                if (records[i][fieldName] == null) records[i][fieldName] = i;
            }
        }
        return records;
    },

    isMissingPrimaryKeys : function(record) {
        return this.getMissingPrimaryKeys(record).length > 0;
    },

    getMissingPrimaryKeys : function(record) {
        var keys = this.getPrimaryKeyFieldNames(),
            missing = [],
            undef;
        if (!record) return keys;
        for (var i = 0; i < keys.length; i++) {
            if (record[keys[i]] == undef) {
                missing.add(keys[i]);
            }
        }
        return missing;
    },

    //<LocalDS

    // Local Filtering
    // --------------------------------------------------------------------------------------------

    // given values for the primary key fields, find the index of the unique matching record
    findByKeys : function (record, recordSet, pos, endPos) {
        if (record == null || recordSet == null) return -1;

        var keys = this.getPrimaryKeyFieldNames(),
            filtered = {},
            dateKeys = false;

        // If we've been passed a ResultSet, the best we can do is search its current localData
        if (isc.ResultSet && isc.isA.ResultSet(recordSet)) recordSet = recordSet.localData;
        for (var i = 0; i < keys.length; i++) {
            filtered[keys[i]] = record[keys[i]];
            if (this.getField(keys[i]).type == "date" || this.getField(keys[i]).type == "time" ||
                this.getField(keys[i]).type == "datetime")
            {
                dateKeys = true;
            }
        }

        // Array.findByKeys() is much faster than DataSource.recordsMatchingFilter(), but it
        // doesn't work with key fields of date type
        if (!dateKeys) return recordSet.findByKeys(record, this, pos, endPos);

        var matching = this.recordIndicesMatchingFilter(recordSet, filtered, {}, pos, endPos);
        if (!matching || matching.length == 0) return -1;
        if (matching.length > 1) {
            this.logWarn("Searching by primaryKey, found " + matching.length + " records " +
                         "when there should only be one - returning the first.  PrimaryKey " +
                         "set was: " + isc.echoAll(filtered));
        }
        return matching[0];
    },

    //> @method dataSource.applyFilter()
    // Returns records in the passed Array that match the provided filter
    // +link{type:Criteria,criteria}. Handles simple or +link{AdvancedCriteria,advanced} criteria.
    // <P>
    // By default:
    // <ul>
    // <li> any criteria that do not correspond to a DataSource field are ignored
    // <li> for simple criteria, any null or empty string criteria are ignored and
    //      all other criteria are passed to +link{fieldMatchesFilter()}
    // <li> for advanced criteria, each criterion is evaluated via +link{evaluateCriterion()}
    // </ul>
    // This method is called by +link{resultSet.applyFilter()} to provide filtering when a
    // ResultSet has a complete cache and filtering can be performed client-side.  You may want
    // to override this method in order to mimic the filtering behavior that your server performs.
    //
    // @param   data        (Array of Record)     the list of rows
    // @param   criteria    (Criteria)  the filter criteria
    // @param   [requestProperties]   (DSRequest Properties)  optional dataSource request properties
    // @return  (Array)     the list of matching rows
    // @visibility external
    //<
    applyFilter : function (data, criteria, requestProperties) {
        var output = [];
        if (!data || data.length == 0) return output;

        // canonicalize certain relative-date-based criteria

        if (this.autoConvertRelativeDates     == true &&
            this.autoConvertRelativeDatesMode != "server")
        {
            if (this.logIsInfoEnabled("relativeDates")) {
                this.logInfo("Calling convertRelativeDates from applyFilter - data is\n\n" +
                             isc.echoFull(criteria));
            }
            criteria = this.convertRelativeDates(criteria);

            if (this.logIsInfoEnabled("relativeDates")) {
                this.logInfo("Called convertRelativeDates from applyFilter - data is\n\n" +
                             isc.echoFull(criteria));
            }
        }

        // If our criteria object is of type AdvancedCriteria, go down the new
        // AdvancedFilter codepath
        if (this.isAdvancedCriteria(criteria)) {
            var normalizedCriteria = this.normalizeAdvancedCriteria(criteria);
            return this.recordsMatchingAdvancedFilter(data, normalizedCriteria, requestProperties);
        }

        // go through the list of items and add any items that match the criteria to the
        // output
        return this.recordsMatchingFilter(data, criteria, requestProperties);
    },

    //> @attr dataSource.dropUnknownCriteria (Boolean : true : IR)
    // If the criteria applied to a fetch type operation contain fields that are not present
    // in the dataSource, should they be ignored when performing filtering on the client.
    // This property is useful for cases where you custom server logic makes use of criteria
    // values to determine what set of records to return to the client, but the data
    // does not actually have record values for these fields and as such the client-side filtering
    // logic should ignore them.
    // @visibility external
    //<
    dropUnknownCriteria:true,

    // return the indices of the records matching the criteria passed in
    recordIndicesMatchingFilter : function (records, filter, requestProperties, startPos, endPos) {
        return this.recordsMatchingFilter(records, filter, requestProperties, startPos, endPos, true);
    },

    // return the records matching the criteria passed in
    recordsMatchingFilter : function (records, filter, requestProperties, startPos, endPos, returnIndices) {
        var filterFields = isc.getKeys(filter),
            filterFieldsLength = filterFields.length,
            matches = [],
            record,
            match,
            fieldName,
            fieldValue,
            filterValue,
            j;

        startPos = startPos || 0;
        endPos = endPos || records.length;

        // Get any customCriteriaField definition from the operationBinding for later checking
        if (requestProperties && requestProperties.operation && this.operationBindings) {

            var op = requestProperties.operation;
            if (op.ID == op.dataSource + "_" + op.type) {
                var opBinding = this.operationBindings.find({operationId:null, operationType:op.type});
            } else {
                var opBinding = this.operationBindings.find({
                    operationId: requestProperties.operation.ID,
                    operationType: op.type
                });
            }

            if (opBinding) {
                var customCriteriaFields = opBinding.customCriteriaFields;
                if (isc.isA.String(customCriteriaFields)) {
                    customCriteriaFields = customCriteriaFields.split(",");
                    // Better trim them...
                    for (var k = 0; k < customCriteriaFields.length; k++) {
                        customCriteriaFields[k] = customCriteriaFields[k].replace(/^\s+|\s+$/g, '');
                    }
                }
            }
        }

        for (var i = startPos; i < endPos; i++) {
            record = records[i];
            if (record == null) continue;
            match = true;

            for (j = 0; j < filterFieldsLength; j++) {
                fieldName = filterFields[j];
                if (fieldName == null) continue; // null property in criteria
                // Pass in the param to handle being passed a dataPath
                var field = this.getField(fieldName, true);
                if (field == null) field = this.getFieldForDataPath(fieldName);
                // Ignore fields that aren't present in the DataSource.  This allows criteria
                // unrelated to the fields to be present but not interpreted by DataSource
                // filtering (eg, possibly only interpreted by server filtering)
                // Exception - if a dataPath is specified that leads to a nested field,
                // allow filtering to proceed (we'll use the dataPath to extract the value from the
                // record directly).
                if (this.dropUnknownCriteria && !field) {
                    continue;
                }

                // Obey the DataSourceField.customSQL and OperationBinding.customCriteriaFields
                // properties
                var skipCustomSQLCheck = false;
                if (isc.isA.List(customCriteriaFields) && customCriteriaFields.contains(fieldName)) {
                    skipCustomSQLCheck = true;
                }

                if (!skipCustomSQLCheck && field && field.customSQL) continue;

                fieldValue = isc.DataSource.getPathValue(record, fieldName, field);
                filterValue = filter[fieldName];
                if (!this.fieldMatchesFilter(fieldValue, filterValue, requestProperties,
                     (field ? field.ignoreTextMatchStyle : null)))
                {
                    match = false;
                    break;
                }
            }
            if (match) matches.add(returnIndices ? i : record);
        }
        return matches;
    },

    recordMatchesFilter : function (record, criteria, requestProperties) {
        if (this.isAdvancedCriteria(criteria)) {
            return this.recordsMatchingAdvancedFilter([record], criteria, requestProperties).length > 0;
        }
        return this.recordsMatchingFilter([record], criteria, requestProperties).length > 0;
    },

    //> @method dataSource.fieldMatchesFilter()
    // Compares a criteria value to a field value and returns whether they match, as follows:
    // <ul>
    // <li> any non-String filter value is directly compared (==) to the field value
    // <li> any String filter value is compared according to
    //      +link{dsRequest.textMatchStyle} in the passed <code>requestProperties</code>,
    //      regardless of the actual field type
    // <li> if the filter value is an Array, the comparison is a logical OR.  If textMatchStyle
    //      is "exact", it matches if fieldValue (or any of it's entries, if it's also an array)
    //      is contained in the filterValue Array.  If textMatchStyle if substring, it matches
    //      if any of the entries in filterValue appear as a case-insensitive substring of any
    //      of the entries in fieldValue.
    // <li>Dates are compared as logical dates if either the field value or the filter value is a logical date.
    //     Only if none of them is a logical date they will be compared as standard Dates
    // </ul>
    // @param   fieldValue  (any)    field value to be compared
    // @param   filterValue (any)    filter value to be compared
    // @param   [requestProperties]   (DSRequest Properties)  optional dataSource request properties
    // @return  (boolean)               true if the filter and field values match, false otherwise
    // @visibility external
    //<
    _$startsWith : "startsWith", _$substring : "substring", _$exact:"exact", _$exactCase:"exactCase",
    fieldMatchesFilter : function (fieldValue, filterValue, requestProperties, ignoreTextMatchStyle) {
        // Pick up textMatchStyle from the request, if present
        var textMatchStyle;

        if (ignoreTextMatchStyle) {
            textMatchStyle = this.ignoreTextMatchStyleCaseSensitive ? this._$exactCase : this._$exact;
        } else if (requestProperties) {
            textMatchStyle = requestProperties.textMatchStyle;
        }

        // If filterValue is an array - always treat this as a logical OR, and match either
        // exact or substring, according to textMatchStyle.
        // If fieldValue is an array - assume this is a multiple: true field and find the intersection
        if (isc.isAn.Array(filterValue) || isc.isAn.Array(fieldValue)) {
            if (!isc.isAn.Array(fieldValue)) {
                fieldValue = [fieldValue];
            }

            if (!isc.isAn.Array(filterValue)) {
                filterValue = [filterValue];
            }

            var isDate = isc.isA.Date(filterValue[0]);



            // dates only really work with exact match, so assume that if isDate = true
            if (textMatchStyle == this._$exact || textMatchStyle == this._$exactCase || isDate) {

                if (isDate) {
                    return filterValue.intersectDates(fieldValue).length > 0;
                } else if (textMatchStyle == this._$exactCase) {
                    return filterValue.intersect(fieldValue).length > 0;
                } else {
                    return filterValue.intersectSubstring(fieldValue, true, this._$exact).length > 0;
                }
            } else {

                return filterValue.intersectSubstring(fieldValue, true, textMatchStyle).length > 0;
            }
        }
        // note: in general the fieldValues are part of records that came from the server and are
        // generally of the same type as the DataSource field.  The filterValues come from a form
        // and are generally strings (3/25/02), even if the field is numeric.

        // handle multiple selects (array of filter values)


        if (isc.isA.Date(fieldValue) && isc.isA.Date(filterValue)) {
            if (filterValue.logicalDate || fieldValue.logicalDate)
                return (Date.compareLogicalDates(fieldValue, filterValue) == 0);
            return (Date.compareDates(fieldValue, filterValue) == 0);
        }

        if (!isc.isA.String(fieldValue) && !isc.isA.String(filterValue)) {
            // if both are non-string values, use direct comparison
            // XXX really, we need some way that you can supply a comparison function for comparing
            // two instances of a custom type.

            //>DEBUG
            if (this.logIsDebugEnabled()) {
                this.logDebug("Direct compare: " + fieldValue + "==" + filterValue);
            }
            //<DEBUG
            return (fieldValue == filterValue);
        }
        // Convert nulls to emptyStrings

        if (filterValue == null) filterValue = isc.emptyString;
        if (fieldValue == null) fieldValue = isc.emptyString;

        // if the field is a string or is numeric, we want to do substring match.  Thus "part"
        // matches "part number" and "17" matches "017395".

        // convert both field and filter value to strings (note: they need to be strings because we
        // actually call string methods on them)
        if (!isc.isA.String(fieldValue)) fieldValue = fieldValue.toString();
        if (!isc.isA.String(filterValue)) filterValue = filterValue.toString();

        // Deprecate the undocumented property "filterIsCaseSensitive" in favor of the new
        // "exactCase" textMatchStyle.  However, for backcompat, continue to honor it if the
        // textMatchStyle is not "exactCase" and filterIsCaseSensitive has been set explicitly
        // to true
        if (textMatchStyle != this._$exactCase && this.filterIsCaseSensitive !== true) {
            fieldValue = fieldValue.toLocaleLowerCase();
            filterValue = filterValue.toLocaleLowerCase();
        }

        // Normalize unsupported text match style to the default
        if (!this.supportsTextMatchStyle(textMatchStyle)) {
            // Only warn once for each text match style for this dataSource
            if (!this._warnedUnsupportedTMS) this._warnedUnsupportedTMS = {};
            if (!this._warnedUnsupportedTMS[textMatchStyle]) {
                this.logWarn("Text match style specified as '" + textMatchStyle +
                             "': This is not supported for" +
                             " this dataSource - performing a substring match instead");
                this._warnedUnsupportedTMS[textMatchStyle] = true;
            }
            textMatchStyle = this.getTextMatchStyle(textMatchStyle);
        }

        if (textMatchStyle == this._$startsWith) {
            return isc.startsWith(fieldValue, filterValue);
        } else if (textMatchStyle == this._$substring) {
            return isc.contains(fieldValue, filterValue);
        // Default to exact match if textMatchStyle is unset. This matches standard fetch
        // behavior (note, this "exact match" may be case -sensitive or case-insensitive,
        // depending on the textMatchStyle and the deprecated property filterIsCaseSensitive.
        // See the check on that property, about 30 lines above)
        } else {
            return fieldValue == filterValue;
        }
    },

    _$iscServer:"iscServer",
    //> @method  dataSource.supportsTextMatchStyle() (A)
    // Does this dataSource support the specified "textMatchStyle" when performing a filter
    // operation against a text field.
    // @param textMatchStyle (TextMatchStyle) textMatchStyle to check. If passed a null value,
    //      assume an exact match is being requested.
    // @visibility external
    //<
    supportsTextMatchStyle : function (style, fetchMode) {
        // Assume completely custom DataSources can handle any textMatchStyle, as they can
        // be completely custom coded
        if (!this.clientOnly && (this.dataFormat != this._$iscServer)) return true;

        // return false if passed an unrecognized textMatchStyle string for standard
        // ISCServer / client only dataSources
        return (style == null || style == this._$substring || style == this._$exact ||
                style == this._$startsWith || style == this._$exactCase);
    },

    // getTextMatchStyle()
    // Normalizes unsupported text match style to a default
    getTextMatchStyle : function (style) {

        if (style == null) style = this._$exact;
        if (!this.supportsTextMatchStyle(style)) {

            style = this._$substring;
        }
        return style;
    },

    // compareTextMatchStyle()
    // When changing to a new text match style, is the new style 'less restrictive'?
    // Returns 0 if the text match style is unchanged,
    // -1 if the filter is less restrictive (So we would have to hit the server for new data
    // even if new criteria are unchanged or more restrictive)
    // 1 if the filter is more restrictive (meaning depending on the criteria we should be
    // able to perform a local filter of the client data when new criteria are set).
    compareTextMatchStyle : function (newStyle, oldStyle) {
        newStyle = this.getTextMatchStyle(newStyle);
        oldStyle = this.getTextMatchStyle(oldStyle);

        // Note: If new and old style match we'll look at the criteria to determine
        // whether a server fetch will be required.
        if (newStyle == oldStyle) return 0;

        // exactCase is the most restrictive, then exact, then startsWith, then substring
        if (newStyle == this._$exactCase) return 1;
        if (oldStyle == this._$exactCase) return -1;
        if (oldStyle == this._$substring) return 1;
        if (newStyle == this._$substring) return -1;
        if (newStyle == this._$exact && oldStyle == this._$startsWith) return 1;
        return -1;
    },

    //> @method dataSource.compareCriteria()
    // Given two sets of criteria, determine whether they are equivalent, the new criteria is
    // guaranteed more restrictive, or the new criteria is not guaranteed more restrictive,
    // returning 0, 1 or -1 respectively.
    // <P>
    // Comparisons between +link{AdvancedCriteria} are made via recursively calling
    // +link{Operator.compareCriteria()} for all criteria involved.
    // <P>
    // For simple +link{Criteria}, by default (+link{criteriaPolicy}:"dropOnShortening"), returns:
    // <ul>
    // <li> -1 if the new criteria has fewer properties than the old criteria (indicating that it
    //      isn't more restrictive)
    // <li> -1 if the value for any property in the old criteria is an array and 1) the value for
    //      the same property in the new criteria isn't an array, or 2) is an array but
    //      of different length, or 3) the arrays do not contain the exact same set of objects
    //      (order can be different)
    // <li> -1 if the value for any given property in the old criteria is not an array, and the
    //      the value for the same property property in the new criteria is different
    // <li> -1 if both values for a given property are strings and the new criteria value doesn't
    //      contain the old criteria value
    // <li> 1 if none of the above are true and, for at least one of the properties, the
    //      respective criteria values are both strings, and the old criteria value is a substring
    //      of, and is shorter than, the new criteria value
    // <li> 0 otherwise (indicating the sets of criteria are equivalent)
    // </ul>
    // <P>
    // For (+link{criteriaPolicy}:"dropOnChange"), returns:
    // <ul>
    // <li> -1 if the two sets of criteria have a different number of properties
    // <li> -1 if the value for any property in the old criteria is an array and 1) the value for
    //      the same property in the new criteria isn't an array, or 2) is an array but
    //      of different length, or 3) the arrays do not contain the exact same set of objects
    //      (order can be different)
    // <li> -1 if the value for any given property in the old criteria is not an array, and the
    //      the value for the same property in the new criteria is different
    // <li> 0 otherwise (indicating the sets of criteria are equivalent)
    // </ul>
    // This method is called by +link{resultSet.compareCriteria()} to determine whether a change
    // in criteria should cause the cache to be invalidated. You may want
    // to override this method in order to mimic the filtering behavior that your server performs.
    //
    // @param   newCriteria     (Criteria)  new filter criteria
    // @param   oldCriteria     (Criteria)  previous filter criteria
    // @param   [requestProperties]     (DSRequest Properties)  dataSource request properties
    // @param   [policy]        (string)    overrides +link{criteriaPolicy}
    // @return  (Number)    0 if the filters are equivalent, 1 if newCriteria is guaranteed more
    //                      restrictive, and -1 if newCriteria is not guaranteed more restrictive
    // @see criteriaPolicy
    // @visibility external
    //<
    compareCriteria : function (newCriteria, oldCriteria, requestProperties, policy) {
        //>DEBUG
        if (this.logIsInfoEnabled()) {
            this.logInfo("Comparing criteria, oldCriteria:\n" + this.echo(oldCriteria) +
                         "\nnewCriteria:\n" + this.echo(newCriteria) +
                         ", policy: " + (policy || this.criteriaPolicy));
        }
        //<DEBUG

        if (oldCriteria == null) return -1;

        // get the textMatchStyle to be used with simple criteria
        var textMatchStyle = this.getTextMatchStyle(requestProperties ?
                                                    requestProperties.textMatchStyle : null);

        // If our criteria objects are of type AdvancedCriteria, go down the new
        // AdvancedFilter codepath
        if (this.isAdvancedCriteria(newCriteria) || this.isAdvancedCriteria(oldCriteria)) {
            var undef,
                result;

            if (this.isAdvancedCriteria(newCriteria)) {
                if (this.isAdvancedCriteria(oldCriteria)) {
                    result = this.compareAdvancedCriteria(newCriteria, oldCriteria,
                                                        requestProperties);
                } else {
                    // Special case: if the old criteria was the propertyless object, we're going from
                    // no filter criteria to some filter criteria - so we know we're guaranteed to be more
                    // restrictive (or at least, not to be less restrictive), so we can return 1.
                    var j = 0;
                    for (var i in oldCriteria) {
                        j++; break;
                    }
                    if (j == 0) result = 1;
                }

                // We have a mix of basic and advanced criteria types - convert the basic criteria
                // object to an equivalent AdvancedCriteria.
                // (NOTE - this code is here as a catch-all only. ResultSet.setCriteria() explicitly
                //  converts basic criteria to AdvancedCriteria as required, so we should never get as
                //  far as this)
                if (result == undef) {
                    oldCriteria = isc.DataSource.convertCriteria(oldCriteria, textMatchStyle);
                    result = this.compareAdvancedCriteria(newCriteria, oldCriteria,
                                                        requestProperties);
                }
            } else {
                // Special case: if the new criteria was the propertyless object, we're going from
                // meaningful criteria to no filter criteria - so we know we're guaranteed to be less
                // restrictive -1
                var j = 0;
                for (var i in newCriteria) {
                    j++; break;
                }
                if (j == 0) {
                    result = -1;
                } else {
                    // We have a mix of basic and advanced criteria types - convert the basic criteria
                    // object to an equivalent AdvancedCriteria
                    newCriteria = isc.DataSource.convertCriteria(newCriteria, textMatchStyle);
                    result = this.compareAdvancedCriteria(newCriteria, oldCriteria,
                                                        requestProperties);
                }
            }
            // Catch-all - result should never be undefined at this point
            if (result == undef) result = -1;

            policy = policy || this.criteriaPolicy;

            if (policy == "dropOnShortening") {
                return result;
            } else {
                return result == 0 ? 0 : -1;
            }
        }

        policy = policy || this.criteriaPolicy;

        if (policy == "dropOnShortening") {
            // If we're doing an exact match, rather than a substring/startsWith match,
            // any change to existing criteria will require a fresh set of data from the server.
            // (Note that introducing filtering to previously unrestricted fields will still be
            // more restrictive so won't require a fetch)
            if (textMatchStyle == this._$exact || textMatchStyle == this._$exactCase) {
                return this.dropOnFieldChange(newCriteria, oldCriteria, requestProperties);
            } else {
                return this.dropOnShortening(newCriteria, oldCriteria, requestProperties);
            }
        } else {
            return this.dropOnChange(newCriteria, oldCriteria, requestProperties);
        }
    },

    //> @method DataSource.getFieldCriterion
    // Returns the depth-first match of a criterion matching the given fieldName.
    //
    // @param criterion (Criteria) the criteria to search
    // @param fieldName (String) the fieldName to find criteria for
    // @return (Criteria) the depth-first matching criterion for the passed fieldName
    // @visibility external
    //<
    getFieldCriterion : function (criterion, fieldName) {
        var result;

        if (criterion.criteria) {
            for (var i = 0; i < criterion.criteria.length; i++) {
                result = this.getFieldCriterion(criterion.criteria[i], fieldName);
                if (result) return result;
            }
        } else {
            if (criterion.fieldName == fieldName) return criterion;
        }

        return null;
    },

    dropOnChange : function (newCriteria, oldCriteria, requestProperties) {


        if (isc.getKeys(oldCriteria).length != isc.getKeys(newCriteria).length) return -1;
        for (var field in oldCriteria) {
            var oldCriteriaValue = oldCriteria[field],
                newCriteriaValue = newCriteria[field];
            if (isc.isAn.Array(oldCriteriaValue)) {

                if (!isc.isAn.Array(newCriteriaValue)) return -1;
                if (oldCriteriaValue.length != newCriteriaValue.length) return -1;
                // the intersection of two identical arrays should be the same length as either of
                // the original arrays
                if (oldCriteriaValue.intersect(newCriteriaValue).length != oldCriteriaValue.length)
                {
                    return -1;
                }
            } else if (isc.isA.Date(oldCriteriaValue) && isc.isA.Date(newCriteriaValue))
            {
                if (oldCriteriaValue.getTime() != newCriteriaValue.getTime()) return -1;
            } else if (oldCriteriaValue != newCriteriaValue) {
                return -1;
            }
        }
        return 0;
    },


    // dropOnFieldChange() - used for the case where dropOnChange is not true but filter type is
    // exact rather than substring match
    // - if any fields in the old criteria have changed, drop cache
    // - if any new non-dataSource fields have been introduced, drop cache
    // - can retain cache if new criteria restrict previously unrestricted fields (in this case
    //   filter is definitely more restricted)
    dropOnFieldChange : function (newCriteria, oldCriteria, requestProperties) {

        var newFields = isc.getKeys(newCriteria),
            oldFields = isc.getKeys(oldCriteria),
            numFieldsDifference = newFields.length - oldFields.length
        ;

        // if new criteria has fewer properties, then it's not more restrictive
        if (numFieldsDifference < 0) return -1;


        // Ensure that existing criteria are unchanged - otherwise we'll need to drop cache
        for (var field in oldCriteria) {
            var oldCriteriaValue = oldCriteria[field],
                newCriteriaValue = newCriteria[field];
            // If we've dropped a field entirely from the criteria filter is less restrictive
            // (hit server)
            if (newCriteriaValue == null) return -1;

            if (isc.isAn.Array(oldCriteriaValue)) {

                if (!isc.isAn.Array(newCriteriaValue)) return -1;
                if (oldCriteriaValue.length != newCriteriaValue.length) return -1;
                // the intersection of two identical arrays should be the same length as either of
                // the original arrays
                if (oldCriteriaValue.intersect(newCriteriaValue).length != oldCriteriaValue.length)
                {
                    return -1;
                }
            } else if (isc.isA.Date(oldCriteriaValue) && isc.isA.Date(newCriteriaValue))
            {
                if (oldCriteriaValue.getTime() != newCriteriaValue.getTime()) return -1;
            } else if (oldCriteriaValue != newCriteriaValue) {
                return -1;
            }
        }

        // At this point we know the old criteria are unchanged.
        // If the new criteria has more properties, and does not contain any non-DS properties
        // not present in the old criteria, then it must be more restrictive
        if (numFieldsDifference > 0) {
            newFields.removeList(oldFields);
            // a non-DS field present in newCriteria and not oldCriteria means potentially less
            // restrictive criteria
            for (var i = 0; i < newFields.length; i++) {
                if (this.getField(newFields[i]) == null) return -1;
            }
            return 1;
        }
        return 0;
    },

    dropOnShortening : function (newCriteria, oldCriteria, requestProperties) {
        var newFields = isc.getKeys(newCriteria),
            oldFields = isc.getKeys(oldCriteria),
            numFieldsDifference = newFields.length - oldFields.length
        ;

        // if new criteria has fewer properties, then it's not more restrictive
        if (numFieldsDifference < 0) return -1;
        var result = 0;
        for (var field in oldCriteria) {
            var oldCriteriaValue = oldCriteria[field],
                newCriteriaValue = newCriteria[field];
            if (newCriteriaValue == null) return -1;
            // if a field that is not found in the DS changes when we're in dropOnShortening mode,
            // consider that as potentially less restrictive criteria.
            // Also, a field marked ignoreTextMatchStyle is less restrictive if it changes
            if ((this.getField(field) == null || this.getField(field).ignoreTextMatchStyle) &&
                                oldCriteriaValue != newCriteriaValue)
            {
                return -1;
            }
            if (isc.isAn.Array(oldCriteriaValue)) {
                // copied from dropOnChange behavior
                if (!isc.isAn.Array(newCriteriaValue)) return -1;
                if (oldCriteriaValue.length != newCriteriaValue.length) return -1;
                if (oldCriteriaValue.intersect(newCriteriaValue).length != oldCriteriaValue.length)
                {
                    return -1;
                }
            } else if (isc.isA.String(oldCriteriaValue)) {
                if (!isc.isA.String(newCriteriaValue)) return -1;
                if (newCriteriaValue.indexOf(oldCriteriaValue) == -1) return -1;
                if (oldCriteriaValue.length > newCriteriaValue.length) return -1;
                if (oldCriteriaValue.length < newCriteriaValue.length) result = 1;
            } else if (isc.isA.Date(oldCriteriaValue) && isc.isA.Date(newCriteriaValue))
            {
                if (oldCriteriaValue.getTime() != newCriteriaValue.getTime()) return -1;
            } else if (oldCriteriaValue != newCriteriaValue) {
                return -1;
            }
        }
        // at this point, the new criteria is either equivalent or more restrictive, so if the new
        // criteria has more properties, and does not contain any non-DS properties not present
        // in the old criteria, then it must be more restrictive
        if (numFieldsDifference > 0) {
            newFields.removeList(oldFields);
            // a non-DS field present in newCriteria and not oldCriteria means potentially less
            // restrictive criteria
            for (var i = 0; i < newFields.length; i++) {
                if (this.getField(newFields[i]) == null) return -1;
            }
            return 1;
        }
        return result;
    },

    //> @method  dataSource.compareDates() (A)
    // Convenience method to compare two Date objects appropriately, depending on whether the
    // passed-in fieldName refers to a field of +link{type:FieldType,type} "datetime" or
    // "date".  In the former case, the dates are compared using +link{Date.compareDates};
    // in the latter case, or if the supplied fieldName is null or unknown to this DataSource,
    // the dates are compared using +link{Date.compareLogicalDates}.
    // @param date1 (Date) First date in comparison
    // @param date2 (Date) Second date in comparison
    // @param fieldName (String) The name of the field for which the comparison is being run
    // @return (Number) 0 if equal, -1 if first date &gt; second date, 1 if second date &gt;
    //                  first date
    // @visibility external
    //<
    compareDates : function (date1, date2, fieldName, otherFieldName) {
        var field = this.getField(fieldName),
            otherField = otherFieldName ? this.getField(otherFieldName) : null
        ;
        if ((field && isc.SimpleType.inheritsFrom(field.type, "datetime")) ||
                (otherField && isc.SimpleType.inheritsFrom(otherField.type, "datetime")))
        {
            return Date.compareDates(date1, date2);
        } else if ((field && isc.SimpleType.inheritsFrom(field.type, "time")) ||
                (otherField && isc.SimpleType.inheritsFrom(otherField.type, "time")))
        {
            return isc.Time.compareLogicalTimes(date1, date2);
        } else {
            return Date.compareLogicalDates(date1, date2);
        }
    },

    // Convenience method for use by the AdvancedCriteria operator methods - helps to enable
    // more compact code, because dates need to be compared in a different way to values of
    // other types
    compareValues : function (value1, value2, fieldName, ignoreCase) {
        if (isc.isA.Date(value1) && isc.isA.Date(value2)) {
            return this.compareDates(value1, value2, fieldName);
        } else {
            var v1 = ignoreCase && value1.toLowerCase ? value1.toLowerCase() : value1,
                v2 = ignoreCase && value2.toLowerCase ? value2.toLowerCase() : value2;
            // Javascript does not consider null to be less than "some string", though it does
            // consider null to be less than "1".  Work around this anomalous behavior.
            if (v1 == null && v2 != null) return 1;
            if (v1 != null && v2 == null) return -1;
            // NOTE: The special return value 2 means that we've been asked to compare two values
            // that are not equal but also are not sensibly "greater than" or "less than"
            // one another - for example "Blink" and 182.
            return v1 > v2 ? -1 : (v1 < v2 ? 1 : (v1 == v2 ? 0 : 2))
        }
    },

    //> @method dataSource.recordsAreEqual
    // Convenience method to test if two records are equal. Testing is done only for the
    // fields defined in the DataSource, anything else is ignored.
    //
    // @param record1 (any) record to be compared against.
    // @param record2 (any) record to be compared.
    //
    // @return (boolean) true if the records are equal, false otherwise.
    // @visibility external
    //<
    recordsAreEqual: function(record1, record2) {
        var fieldNames = this.getFieldNames();
        for (var i = 0; i <  fieldNames.length; i++) {
            if (this.compareValues(record1[ fieldNames[i] ], record2[ fieldNames[i] ], fieldNames[i]) != 0) {
                return false;
            }
        }
        return true;
    },

    //> @method dataSource.convertDataSourceCriteria()
    // Converts criteria expressed in SmartClient's simple criteria format to an
    // AdvancedCriteria object.  This instance method differs from the class method
    // +link{classMethod:DataSource.convertCriteria} in that it makes use of the dataSource
    // as schema to help in the conversion.  For example, this method is able to honor
    // +link{dataSourceField.ignoreTextMatchStyle} and use the dataSource's
    // +link{defaultTextMatchStyle,defaultTextMatchStyle} rather than assuming "substring"
    //
    // @param criteria (Criteria) simple criteria
    // @param [textMatchStyle] (TextMatchStyle) default style of matching text.  Defaults to
    //                                          the dataSource's defaultTextMatchStyle
    // @return (AdvancedCriteria) equivalent AdvancedCriteria object
    // @visibility external
    //<
    convertDataSourceCriteria : function(criteria, textMatchStyle) {
        return isc.DataSource.convertCriteria(criteria, textMatchStyle, this);
    }
});

// AdvancedFilter additions for extended local filtering
// --------------------------------------------------------------------------------------------

//> @object AdvancedCriteria
// AdvancedCriteria is a format for representing search criteria which may include
// operators on field values such as "less than", or may include sub-clauses such as several
// criteria applied to fields joined by an "OR" operator.
// <P>
// SmartClient DataSources can use AdvancedCriteria to search a list of +link{Record}s, and
// the SmartClient Java Server can translate AdvancedCriteria to either SQL or Hibernate
// queries (<b>Note:</b> The server-side AdvancedCriteria handling feature is only available
// with the <b>Power</b> and <b>Enterprise</b> Editions of SmartClient; the Pro Edition is
// limited to ordinary criteria handling on the server side).<p>
// If the entire dataset is cached locally, SmartClient can perform AdvancedCriteria filtering
// on the client, avoiding a server call.
// <P>
// <smartclient>
// An AdvancedCriteria is an ordinary JavaScript object which can be created directly
// with JavaScript literal notation.  For example:
// <pre>
// var advancedCriteria = {
//        _constructor:"AdvancedCriteria",
//        operator:"and",
//        criteria:[
//            // this is a Criterion
//            { fieldName:"salary", operator:"lessThan", value:"80000" },
//            { operator:"or", criteria:[
//                  { fieldName:"title", operator:"iContains", value:"Manager" },
//                  { fieldName:"reports", operator:"notNull" }
//              ]
//            }
//        ]
//    }
// </pre>
// An AdvancedCriteria is in effect a +link{Criterion} that has been marked with
// _constructor:"AdvancedCriteria" to mark it as complete criteria.
// <P>
// This makes AdvancedCriteria very easy to store and retrieve as JSON strings, using
// +link{JSON.encode,JSONEncoder}.
// </smartclient>
// <smartgwt>
// AdvancedCriteria objects can be created directly in java. For example:
// <pre>
// AdvancedCriteria criteria = new AdvancedCriteria(OperatorId.AND, new Criterion[]{
//     new Criterion("salary", OperatorId.LESS_THAN, 8000),
//     new AdvancedCriteria(OperatorId.OR, new Criterion[]{
//         new Criterion("title", OperatorId.ICONTAINS, "Manager"),
//         new Criterion("reports", OperatorId.NOT_NULL)
//     })
// });
// </pre>
// </smartgwt>
// <P>
// In addition to building a raw AdvancedCriteria object as described above, the
// +link{DataSource.convertCriteria()} and +link{DataSource.combineCriteria()} methods
// may be used to create and modify criteria based on simple fieldName / value mappings.
// <P>
// When passed to the SmartClient Server, a server-side AdvancedCriteria instance (in the
// package com.isomorphic.criteria) can be retrieved from a DSRequest via
// com.isomorphic.datasource.DSRequest.getAdvancedCriteria().  These same AdvancedCriteria
// objects can be directly created server side, and applied to a DSRequest via
// setAdvancedCriteria().
// <P>
// +link{RestDataSource}, the recommended way of integration with servers that are not running
// the SmartClient Server Framework, defines a standard XML and JSON serialization of
// <code>AdvancedCriteria</code>.
// <P>
// For other servers, you can translate <code>AdvancedCriteria</code> into whatever format is
// expected by the server, typically by implementing +link{dataSource.transformRequest()}.
// <P>
// <smartgwt>
// The internal representation of AdvancedCriteria is a simple JavaScript structure, available
// via AdvancedCriteria.getJsObj():
// <pre>
// // an AdvancedCriteria
// {
//     _constructor:"AdvancedCriteria",
//     operator:"and",
//     criteria:[
//         // this is a Criterion
//         { fieldName:"salary", operator:"lessThan", value:"80000" },
//         { operator:"or", criteria:[
//             { fieldName:"title", operator:"iContains", value:"Manager" },
//             { fieldName:"reports", operator:"notNull" }
//           ]
//         }
//     ]
// }
// </pre>
// And an AdvancedCriteria can also be created from a JavaScriptObject.  This makes
// AdvancedCriteria very easy to store and retrieve as JSON strings, using
// +link{JSON.encode,JSONEncoder}.
// </smartgwt>
// See +link{group:criteriaEditing,Criteria Editing} for information about
// editing AdvancedCriteria in a DynamicForm.
//
// @inheritsFrom Criterion
// @group advancedFilter
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<


//> @object Criterion
// An object representing a criterion to apply to a record.
// <P>
// A criterion is part of the definition of an +link{AdvancedCriteria} object, which is used to
// filter records according to search criteria.
// <P>
// A criterion consists of an +link{criterion.operator} and typically a
// +link{dataSourceField.name,fieldName} from a
// +link{Record} and a +link{criterion.value,value} to compare to.  However some operators
// either don't require a value (eg, isNull) or act on other criteria rather than directly on a
// +link{Record}'s fields (eg, the "and" and "or" logical operators).
// <P>
// A shortcut form is also allowed where only <code>fieldName</code> and <code>value</code>
// values are provided. In this case the <code>operator</code> is assumed to be "equals".
// @group advancedFilter
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<

//> @attr criterion.operator (OperatorId : null : IR)
// Operator this criterion applies.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr criterion.fieldName (String : null : IR)
// Name of the field in each +link{Record} that this criterion applies to.  Not applicable for a
// criterion with +link{criterion.criteria,sub-criteria}. Can be specified as a dataPath to
// allow matching nested objects. Use '/' as delimiters for dataPath. See
// +link{type:DataPath,dataPath} for more information.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr criterion.value (any : null : IR)
// Value to be used in the application of this criterion.
// <P>
// Value may be required or not required, or may be an Array, according to the
// +link{type:OperatorValueType} of the operator.
//
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr criterion.criteria (Array of Criterion : null : IR)
// For a criterion with an operator that acts on other criteria (eg "and", "or"), a list of
// sub-criteria that are grouped together by the operator.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr criterion.start (any : null : IR)
// Start value of a criterion with an operator of type <code>"valueRange"</code>.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<


//> @attr criterion.end (any : null : IR)
// End value of a criterion with an operator of type <code>"valueRange"</code>.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

// Operators
// ---------------------------------------------------------------------------------------

//> @type OperatorValueType
// Indicates the kind of value expected in a +link{Criterion} that uses this operator.
//
// @value "fieldType" +link{criterion.value} should contain a value of the same type as the
//                    field.
// @value "fieldName" +link{criterion.value} should be the name of another field in the record
// @value "none"      no criterion.value or other setting required (used for operators like
//                    isNull).
// @value "criteria" +link{criterion.criteria} should be an Array of criteria (used for logical
//                   operators like "and").
// @value "valueRange" +link{criterion.start} and +link{criterion.end} should contain start and
//                     end values, both of the same type as the field.
// @value "valueSet" +link{criterion.value} should contain an Array of valid field values.
// @value "custom" +link{criterion.value} should contain a value which is not necessarily the
//                 same type as the field (used for regexp and similar operators).
//                 +link{operator.editorType} can be specified as a FormItem to use to
//                 enter a value for the criterion.
// @group advancedFilter
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<

//> @type OperatorId
// An operator is used as part of a +link{Criterion} when specifying +link{AdvancedCriteria}.
// <P>
// This list of operators indicates the set of operators built into SmartClient DataSources,
// which can be used for both client and server-side filtering.   You can extend the list of
// operators with +link{dataSource.addSearchOperator()}.
//
// @value "equals" exactly equal to
// @value "notEqual" not equal to
// @value "iEquals" exactly equal to, if case is disregarded
// @value "iNotEqual" not equal to, if case is disregarded
// @value "greaterThan" Greater than
// @value "lessThan" Less than
// @value "greaterOrEqual" Greater than or equal to
// @value "lessOrEqual" Less than or equal to
// @value "contains" Contains as sub-string (match case)
// @value "startsWith" Starts with (match case)
// @value "endsWith" Ends with (match case)
// @value "iContains" Contains as sub-string (case insensitive)
// @value "iStartsWith" Starts with (case insensitive)
// @value "iEndsWith" Ends with (case insensitive)
// @value "notContains" Does not contain as sub-string (match case)
// @value "notStartsWith" Does not start with (match case)
// @value "notEndsWith" Does not end with (match case)
// @value "iNotContains" Does not contain as sub-string (case insensitive)
// @value "iNotStartsWith" Does not start with (case insensitive)
// @value "iNotEndsWith" Does not end with (case insensitive)
// @value "iBetween" shortcut for "greaterThan" + "and" + "lessThan" (case insensitive)
// @value "iBetweenInclusive" shortcut for "greaterOrEqual" + "and" + "lessOrEqual" (case insensitive)
// @value "matchesPattern" Basic GLOB matching using wildcards
//        (see +link{DataSource.translatePatternOperators} for more information on available patterns)
// @value "iMatchesPattern" Basic GLOB matching using wildcards (case insensitive)
//        (see +link{DataSource.translatePatternOperators} for more information on available patterns)
// @value "containsPattern" GLOB matching using wildcards. Value is considered to meet the
//        criterion if it contains the pattern. See +link{DataSource.translatePatternOperators}
//        for more information on available patterns)
// @value "iContainsPattern" GLOB matching using wildcards. Value is considered to meet the
//        criterion if it contains the pattern. Matching is case insensitive. See
//        +link{DataSource.translatePatternOperators} for more information on available patterns)
// @value "regexp" Regular expression match
// @value "iregexp" Regular expression match (case insensitive)
// @value "isNull" value is null
// @value "notNull" value is non-null.  Note empty string ("") is non-null
// @value "inSet" value is in a set of values.  Specify criterion.value as an Array
// @value "notInSet" value is not in a set of values.  Specify criterion.value as an Array
// @value "equalsField" matches another field (match case, specify fieldName as criterion.value)
// @value "notEqualField" does not match another field (match case, specify fieldName as criterion.value)
// @value "iEqualsField" matches another field (case insensitive, specify fieldName as criterion.value)
// @value "iNotEqualField" does not match another field (case insensitive, specify fieldName as criterion.value)
// @value "greaterThanField" Greater than another field (specify fieldName as criterion.value)
// @value "lessThanField" Less than another field (specify fieldName as criterion.value)
// @value "greaterOrEqualField" Greater than or equal to another field
//        (specify fieldName as criterion.value)
// @value "lessOrEqualField" Less than or equal to another field
//        (specify fieldName as criterion.value)
// @value "containsField" Contains as sub-string (match case) another field value
//        (specify fieldName as criterion.value)
// @value "startsWithField" Starts with (match case) another field value
//        (specify fieldName as criterion.value)
// @value "endsWithField" Ends with (match case) another field value
//        (specify fieldName as criterion.value)
// @value "iContainsField" Contains as sub-string (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "iStartsWithField" Starts with (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "iEndsWithField" Ends with (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "notContainsField" Does not contain as sub-string (match case) another field value
//        (specify fieldName as criterion.value)
// @value "notStartsWithField" Does not start with (match case) another field value
//        (specify fieldName as criterion.value)
// @value "notEndsWithField" Does not end with (match case) another field value
//        (specify fieldName as criterion.value)
// @value "iNotContainsField" Does not contain as sub-string (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "iNotStartsWithField" Does not start with (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "iNotEndsWithField" Does not end with (case insensitive) another field value
//        (specify fieldName as criterion.value)
// @value "and" all subcriteria (criterion.criteria) are true
// @value "not" all subcriteria (criterion.criteria) are false
// @value "or" at least one subcriteria (criterion.criteria) is true
// @value "between" shortcut for "greaterThan" + "lessThan" + "and".  Specify criterion.start
//         and criterion.end
// @value "betweenInclusive" shortcut for "greaterOrEqual" + "lessOrEqual" + "and".  Specify
//         criterion.start and criterion.end
// @group advancedFilter
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<


//> @object Operator
// Specification of an operator for use in filtering, for example "equals".
// Use with +link{DataSource.addSearchOperator()} to define custom filtering behaviors for
// client-side filtering.
// @group advancedFilter
// @treeLocation Client Reference/Data Binding/DataSource
// @serverDS allowed
// @visibility external
//<

//> @attr operator.ID (OperatorId : null : IR)
// Unique id for an operator, which appears within +link{AdvancedCriteria} as the
// +link{operator} property.
// <P>
// A list of built-in identifiers is +link{OperatorId,here}.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr operator.title (String : null : IR)
// User-visible title for this operator, such as "doesn't contain".
// <P>
// To simplify internationalization by separating titles from operator code, you can use
// specify +link{operator.titleProperty} instead of this property.
//
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr operator.titleProperty (identifier : null : IR)
// Name of a property on the +link{Operators} class that provides the title for this operator.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<


//> @method operator.condition()
// Method which actually evaluates whether a given record meets a +link{criterion}.
// <P>
// For operators that act on +link{criterion.criteria,sub-criteria}, call
// +link{dataSource.evaluateCriterion()} to evaluate sub-criteria.
// <P>
// Because criteria are sometimes applied to user-entered data that has not been validated, a
// robust <code>condition()</code> function should expect that data found in a +link{Record}
// may be null, NaN, not the correct type (eg "NA" for a type:"date" field) or otherwise out of
// the expected range.
// <P>
// Note that an Operator has access both to the +link{Criterion} object, allowing operators
// that act on more than one field or perform calculations, and access to the +link{Operator}
// object itself, allowing a <code>condition()</code> function to be shared across a range of
// related operators with different +link{OperatorId}s.
//
// @param value (any) value from the field supplied as +link{criterion.fieldName}, if
//   applicable
// @param record (Record) record being evaluated
// @param fieldName (any) fieldName supplied as +link{criterion.fieldName}, if applicable
// @param criterion (Criterion) criterion definition
// @param operator (Operator) operator definition
// @return (boolean) whether the field passes this criteria
// @group advancedFilter
// @visibility external
//<

//> @attr operator.fieldTypes (Array of FieldType : null : IR)
// List of types that this Operator is valid for.
// <P>
// If omitted, the operator is assumed to be valid for all FieldTypes unless a list of
// FieldTypes is passed to +link{DataSource.addSearchOperator}.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr operator.requiresServer (boolean : false : IR)
// Whether this operator needs to be executed on the server side.
// <P>
// This implies that if a +link{criterion} using this operator is either introduced into
// +link{AdvancedCriteria,criteria} or is changed, the server will need to be contacted to
// perform filtering.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr operator.hidden (boolean : false : IR)
// Whether this operator should be offered to users by default in interfaces such as the
// +link{class:FilterBuilder}.
// <P>
// Setting hidden:true means the operator can be used in a programmatic search, for example,
// by calling +link{resultSet.setCriteria()}, but does not appear in the UI.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @method operator.compareCriteria()
// Compare two criteria, both of which use this operator, and report whether the newCriteria is
// definitely more restrictive than the previous criteria.
// <P>
// This is used by the +link{ResultSet} to understand whether client-side filtering can
// continue using cached data, or whether server-side filtering must be used instead.
//
// @param newCriterion (Criterion) new criterion
// @param oldCriterion (Criterion) previous criterion
// @return (Number) 0 if the criteria are equivalent, 1 if newCriterion is guaranteed more
//                      restrictive, and -1 if newCriterion is not guaranteed more restrictive
// @group advancedFilter
// @visibility external
//<

//> @attr operator.valueType (OperatorValueType : null : IR)
// Indicates the kind of value expected in a +link{Criterion} that uses this operator.
// +link{OperatorValueType} lists possibilities.
// <P>
// The default of <code>null</code> is equivalent to "fieldType", indicating that
// +link{criterion.value} is expected to contain a value of the same type as the field
// indicated by +link{criterion.fieldName}.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr operator.editorType (FormItem classname : null : IR)
// For an operator with +link{valueType}:"custom", indicates what kind of FormItem to use to
// provide a user interface for creating a valid +link{criterion}.  The default of
// <code>null</code> means an ordinary TextItem is fine.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @method operator.getCriterion()
// In combination with +link{operator.editorType}, this override point allows you to define a
// client-side only Operator that simply provides a custom UI for creating a Criterion based on
// one of the built-in operators.
// <P>
// For example, the "between" operator allows AdvancedCriteria to be created that can select any
// date range, however in a given application certain specific date ranges might be more
// meaningful (eg "next week", "last quarter") and you might want to offer the user a picker for
// those date ranges.  You could create an operator "presetDateRange" with an editorType
// indicating a custom SelectItem that shows available ranges, and then implement
// operation.getCriterion() to take the value from this SelectItem and produce a Criterion
// selecting the chosen date range.
// <P>
// Note that another approach, if it's not required that this custom interface appear in the
// FilterBuilder, is just to have a separate DynamicForm for picking special date
// ranges, and use +link{DataSource.combineCriteria()} to merge the criteria with the
// FilterBuilder's criteria, as in +explorerExample{dynamicReporting,this sample}.
//
// @param fieldName (String)
// @param item (FormItem)
// @return Criterion
// @visibility external
//<

//> @attr operator.symbol (String : null : IR)
// The text use when using this operator as an
// +link{FormItem.allowExpressions, expression} in a FormItem.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<




// ---------------------------------------------------------------------------------------

//> @attr dataSource.strictSQLFiltering (Boolean : false : IRA)
// If set to true, both client and server-side advanced filtering used by SmartClient will follow
// SQL99 behavior for dealing with NULL values, which is often counter-intuitive to users.
// Specifically, when a field has NULL value, all of the following expressions are false:
// <pre>
//    field == "someValue"  (normally false)
//    field != "someValue"  (normally true)
//    not (field == "someValue")   (normally true)
//    not (field != "someValue")   (normally false)
// </pre>
// This property can be overridden per-query by specifying <code>strictSQLFiltering</code>
// directly as a property on the +link{AdvancedCriteria}.
// <p>
// <b>NOTE:</b> On the server side, this property is only applicable if you are using the
// SQL DataSource; the other built-in types (Hibernate and JPA/JPA2) do not offer this mode.
// @serverDS allowed
// @visibility external
//<



isc.DataSource.addClassMethods({

    //> @classMethod DataSource.applyFilter()
    // Returns records in the passed Array that match the provided filter
    // +link{type:Criteria,criteria}. Handles simple or +link{AdvancedCriteria,advanced} criteria.
    // <P>
    // Note that this method differs from the instance method +link{method:dataSource.applyFilter()}
    // in that there will be no field object associated with and of the various criteria passed in.
    //
    // @param   data        (Array)     the list of rows
    // @param   criteria    (Criteria)  the filter criteria
    // @return  (Array)     the list of matching rows
    // @visibility internal
    //<
    // This static implementation is useful for cases where we don't have a DataSource instance
    // but we want to perform simple filtering.


    applyFilter : function (data, criteria) {
        var output = [];
        if (!data || data.length == 0) return output;

        if (this._filterDS == null) {
            this._filterDS = isc.DataSource.create({
                fields:[{name:"_defaultField"}],
                dropUnknownCriteria:false,
                clientOnly:true
            });
        }
        var matches = this._filterDS.applyFilter(data, criteria);
        return matches;
    },

    //> @classMethod DataSource.addSearchOperator()
    // Add a new search operator to all DataSources.
    // <P>
    // See also +link{method:DataSource.addSearchOperator()} for adding operators to specific
    // DataSources only.
    //
    // @param operator (Operator) definition of the operator to add
    // @group advancedFilter
    // @visibility external
    //<
    addSearchOperator : function (operator) {
        if (!operator || !operator.ID) {
            isc.logWarn("Attempted to add null search operator, or operator with no ID");
            return;
        }
        if (!isc.DataSource._operators) isc.DataSource._operators = {};
        var opList = isc.DataSource._operators,
            undef;
        if (opList[operator.ID] !== undef) {
            // This operator is already registered - log a warning and replace it with the
            // passed-in definition
            isc.logWarn("Attempted to add existing operator " + operator.ID + " - replacing");
            //var index = opList.findIndex("ID", operator.ID);
            //if (index >= 0) opList.removeAt(index);
        }
        isc.DataSource._operators[operator.ID] = operator;
    },

    getSearchOperators : function () {
        return isc.DataSource._operators;
    },

    //> @classMethod DataSource.setTypeOperators()
    // Set the list of valid +link{OperatorId}s for a given FieldType.
    //
    // @param typeName (String | FieldType)
    // @param operators (Array[] of OperatorId) available Operators
    // @group advancedFilter
    // @visibility external
    //<
    setTypeOperators : function (typeName, operators) {
        if (!operators ) return;
        if (!isc.isAn.Array(operators) ) operators = [ operators ];
        if (!isc.DataSource._typeOperators ) isc.DataSource._typeOperators = {};
        isc.DataSource._typeOperators[typeName || "_all_"] = operators;
    },

    // _getNextRequestId
    // Increment and return a counter for dsRequests
    // This allows each request to have a unique ID within the page
    _currentRequestId:0,
    _getNextRequestId : function () {
        return this._currentRequestId++;
    },

    //> @classMethod DataSource.getAutoTitle()
    // Utility method to derive a reasonable user-visible title from an identifier.
    // <P>
    // The following approach is taken:
    // <ul>
    // <li> any underscores (_) or dollar signs ($) become spaces, except that there will never
    //      be either a leading or trailing space.
    // <li> if the fieldName is either entirely uppercase or lowercase, all words separated by
    //      spaces are given a leading capital letter.  Example USER_NAME or user_name -> "User
    //      Name".
    // <li> if there is any use of mixed case, camelCaps convention is assumed, and the field name
    //      is split into separate words based on 1) everywhere an uppercase letter appears after a
    //      lowercase letter 2) everywhere a series of uppercase letters ends.  Letter case will
    //      not be modified, with the exception that the first word or any word after an
    //      underscore will have its first letter capitalized.  Examples: useHTTPProxy -> "Use
    //      HTTP Proxy", audit_userName -> "Audit User Name"
    // </ul>
    //
    // @param identifier (String) identifier for which a title is desired.
    // @return (String) auto-derived title
    //
    // @group title
    // @visibility external
    //<
    getAutoTitle : function (identifier, spaceChars) {
        // allow the regex used for space chars to be replaced.  Sometimes eg "-" makes sense
        spaceChars = spaceChars || new RegExp("[_\$]", "g");

        if (!identifier) return "";
        if (!isc.isA.String(identifier)) identifier = identifier.toString();

        var title;
        // Replace underscores with spaces
        work = identifier.replace(spaceChars, " ");
        // Trim whitespace off the beginning and end - this will also get rid of spare
        // whitespace introduced by leading or trailing underscores
        var work = work.replace(/^\s+|\s+$/g, "");
        if (work == work.toUpperCase() || work == work.toLowerCase()) {
            // All one case.  Convert to lowercase and then capitalize the first letter
            // of each word
            work = work.toLowerCase();
            var capNext = true;
            title = "";
            for (var i = 0; i < work.length; i++) {
                var letter = work.substr(i, 1);
                if (capNext) {
                    letter = letter.toUpperCase();
                    capNext = false;
                }
                if (letter == ' ') capNext = true;
                title = title + letter;
            }
        } else {
            // camelCase.  A new word starts every place an upper-case letter follows
            // a lower-case letter, or when a sequence of upper-case letters (eg, HTTP)
            // ends (plus the first letter of the name, of course)
            title = work.substr(0,1).toUpperCase();
            var capLast = work.substr(0,1) == work.substr(0,1).toUpperCase();
            var capSeq = false;
            var lastSpace = false;
            for (var i = 1; i < work.length; i++) {
                var letter = work.substr(i, 1);
                if (capSeq && letter == letter.toLowerCase()) {
                    capSeq = false;
                    title = title.substr(0, title.length - 1) + " " +
                            title.substr(title.length - 1);
                }
                if (capLast && letter == letter.toUpperCase()) {
                    capSeq = true;
                }
                if (!capLast && letter == letter.toUpperCase()) {
                    title = title + " ";
                }
                capLast = letter == letter.toUpperCase();
                if (lastSpace) letter = letter.toUpperCase();
                lastSpace = letter == " ";
                title = title + letter;
            }
        }
        return title;
    },

    //> @classMethod dataSource.convertCriteria()
    // Converts criteria expressed in SmartClient's simple criteria format to an AdvancedCriteria
    // object.
    //
    // @param criteria (Criteria) simple criteria
    // @param [textMatchStyle] (TextMatchStyle) default style of matching text.  Defaults to
    //                                          "substring"
    // @return (AdvancedCriteria) equivalent AdvancedCriteria object
    // @visibility external
    //<
    _typeMap: {exact:"equals",exactCase:"iEquals",substring:"iContains",startsWith:"iStartsWith"},
    convertCriteria : function(criteria, textMatchStyle, ds) {
        var aCriteria = {
            _constructor: "AdvancedCriteria",
            operator: "and"
        }

        if (!textMatchStyle && ds) textMatchStyle = this._typeMap[ds.defaultTextMatchStyle];

        var subCriteria = [];
        for (var fieldName in criteria) {
            var field = ds == null ? null : ds.getField(fieldName),
                defaultType = ds == null ? null : this._typeMap[ds.defaultTextMatchStyle],
                type = null;
            if (field != null) {
                if (field.ignoreTextMatchStyle) {
                    type = ds.ignoreTextMatchStyleCaseSensitive ? "equals" : "iEquals"
                }
            }
            if (isc.isAn.Array(criteria[fieldName])) {

                var disjunct = {
                    _constructor: "AdvancedCriteria",
                    operator: "or",
                    criteria: []
                }
                for (var i = 0; i < criteria[fieldName].length; i++) {
                    var value = criteria[fieldName][i];
                    var operator = type ? type : this.getCriteriaOperator(value,
                                                        textMatchStyle, defaultType);
                    if (value == null &&
                        !this.criteriaConversion_OperatorKeepNullValue[operator]) {
                            continue;
                    }
                    disjunct.criteria.add({
                        fieldName: fieldName,
                        operator: operator,
                        value: value
                    });
                }
                if (disjunct.criteria.length > 0) {
                    subCriteria.add(disjunct);
                }
            } else {
                var value = criteria[fieldName],
                    operator = type ? type : this.getCriteriaOperator(value,
                                                        textMatchStyle, defaultType);
                if (value == null &&
                    !this.criteriaConversion_OperatorKeepNullValue[operator]) {
                        continue;
                }

                subCriteria.add({
                    fieldName: fieldName,
                    operator: operator,
                    value: criteria[fieldName]
                });
            }
        }

        aCriteria.criteria = subCriteria;
        return aCriteria;
    },

    getCriteriaOperator : function (value, textMatchStyle, defaultOperator) {
        var operator;
        if (isc.isA.Number(value) || isc.isA.Date(value) || isc.isA.Boolean(value)) {
            operator = "equals";
        } else if (textMatchStyle == "exactCase") {
            operator = "equals";
        } else if (textMatchStyle == "equals" || textMatchStyle == "exact") {
            operator = "iEquals";
        } else if (textMatchStyle == "startsWith") {
            operator = "iStartsWith";
        } else {
            operator = defaultOperator || "iContains";
        }
        return operator;
    },

    // When converting from a simple criteria to an AdvancedCriteria, how should we handle
    // null values?
    // Basically these are valid in an "equals" type match - the dev may want to test for
    // a field being empty, but invalid in a substring (startwith etc) match.
    // If we encounter a null value in a substring match situation, assume the developer
    // is just explicitly picking up field values from unpopulated fields and skip them.
    // (This is basically an equivalent result to testing for "contains('')" - the
    // criterion will have no meaningful effect, though we don't explicitly skip that case).

    criteriaConversion_OperatorKeepNullValue:{
        "equals":true,
        "notEqual":true,
        "iEquals":true,
        "iNotEqual":true
    },



    //> @type CriteriaCombineOperator
    // The logical operator to use when combining criteria objects with the
    // +link{DataSource.combineCriteria} method.
    //
    // @value "and"
    // @value "or"
    //
    // @visibility external
    //<

    //> @classMethod dataSource.combineCriteria()
    // Combines two criteria (either simple criteria objects or AdvancedCriteria) using the
    // "outerOperator".  Note that the combined criteria object will be an AdvancedCriteria
    // unless: <ul>
    // <li>both input criteria objects are simple, and</li>
    // <li>the "outerOperator" is "and", and</li>
    // <li>there is no collision of key names on the two criteria</li>
    // </ul>
    // @param criteria1 (Criteria) first criteria object
    // @param criteria2 (Criteria) second criteria object
    // @param [outerOperator] (CriteriaCombineOperator) operator to use to combine the criteria.
    //                                          Defaults to "and"
    // @param [textMatchStyle] (TextMatchStyle) style of matching text, if it is necessary to
    //                                          convert a simple criteria object to an
    //                                          AdvancedCriteria.  Defaults to "substring"
    // @return (Criteria) The combined criteria
    // @example dynamicReporting
    // @visibility external
    //<
    // subCriteria param: Sub-criteria of AdvancedCriteria are not marked with
    // _constructor:"AdvancedCriteria", but in some cases we need to combine them into
    // composite criteria, and we should use the "Advanced" type logic (checking for operator,
    // fieldName etc), and avoid marking the generated object as _constructor:"AdvancedCriteria".
    combineCriteria : function (criteria1, criteria2, outerOperator, textMatchStyle, subCriteria) {
        if (!criteria1) return criteria2;
        if (!criteria2) return criteria1;

        if (!outerOperator) outerOperator = "and";

        if (outerOperator != "and" && outerOperator != "or") {
            isc.logWarn("combineCriteria called with invalid outerOperator '" +
                                outerOperator + "'");
            return null;
        }

        var undef, advanced;

        // see if both criteria are simple and we can safely combine them as a simple Criteria
        // Note: can't use isAdvancedCriteria because it's an instance method
        if (!subCriteria && criteria1._constructor != "AdvancedCriteria" &&
            criteria2._constructor != "AdvancedCriteria" &&
            outerOperator == "and")
        {
            for (var key in criteria1) {
                if (criteria2[key] != undef) {
                    advanced = true;
                    break;
                }
            }
        } else {
            advanced = true;
        }

        // return simple criteria by straightforward combination
        if (!advanced) {
            return isc.addProperties({}, criteria1, criteria2);
        }

        // combine into an AdvancedCriteria
        var advCrit1, advCrit2;

        // if either criteria is currently simple, upgrade it to advanced
        if (subCriteria || criteria1._constructor == "AdvancedCriteria") {
            advCrit1 = criteria1;
        } else {
            advCrit1 = isc.DataSource.convertCriteria(criteria1, textMatchStyle);
        }

        if (subCriteria || criteria2._constructor == "AdvancedCriteria") {
            advCrit2 = criteria2;
        } else {
            advCrit2 = isc.DataSource.convertCriteria(criteria2, textMatchStyle);
        }

        // join the two AdvancedCriteria together with the specified outerOperator
        var aCrit = { operator: outerOperator };
        if (!subCriteria) {
            aCrit._constructor = "AdvancedCriteria";
        }

        // Optimization opportunity - if we were passed two criteria with the same
        // operator, and that operator is the same as "outerOperator", we can flatten
        // the structure by removing one intervening level.
        if (advCrit1.operator == outerOperator && advCrit2.operator == outerOperator) {
            aCrit.criteria = [];
            aCrit.criteria.addAll(advCrit1.criteria);
            aCrit.criteria.addAll(advCrit2.criteria);
        } else {
            aCrit.criteria = [advCrit1, advCrit2];
        }

        return aCrit;
    },

    // When we have advanced criteria objects we can have more layers of nesting than are
    // actually necessary
    // For example:
    // {operator:"and",
    //  criteria:[
    //    {fieldName:"x", value:"y", operator:"z"},
    //    {operator:"and",
    //      criteria:[
    //        {fieldName:"a", value:"b", operator:"c"},
    //        {fieldName:"d", value:"e", operator:"f"}
    //      ]
    //     }
    //   ]
    //  }
    // Could have the inner "and" clause eliminated.
    // This method will simplify advanced criteria by flattening nested and / or clauses
    // as much as possible to make it easier to work with the criteria object
    // returned
    simplifyAdvancedCriteria : function (criteria, returnNull) {
        // If we're passed simple criteria don't attempt to do anything to it!
        if (!this.isAdvancedCriteria(criteria)) {
            return criteria;
        }
        criteria = this._simplifyAdvancedCriteria(criteria, returnNull);
        if (criteria == null) return null;
        criteria._constructor = "AdvancedCriteria";
        return criteria;
    },
    _simplifyAdvancedCriteria : function (criteria, clearEmptyCriteria) {
        var operator = criteria.operator;
        if (operator == "and" || operator == "or") {
            var innerCriteria = criteria.criteria;

            // Sanity check - we don't really expect to see empty criteria array as it'd
            // be meaningless
            if (innerCriteria == null || innerCriteria.length == 0) {
                if (clearEmptyCriteria) return null;
                return criteria;
            }

            // Single item in an "and" / "or" block means the outer block is unnecessary
            if (innerCriteria.length == 1) {
                criteria = innerCriteria[0];
                return this._simplifyAdvancedCriteria(criteria, true);
            }

            // Nested 'and'/'or' blocks - can combine upwards
            var newInnerCriteria = [];
            for (var i = 0; i < innerCriteria.length; i++) {
                var innerCrit = innerCriteria[i];
                // simplify recursively
                innerCrit = this._simplifyAdvancedCriteria(innerCrit, true);
                if (innerCrit != null) {
                    if (innerCrit.operator == operator) {
                        newInnerCriteria.addList(innerCrit.criteria);
                    } else {
                        newInnerCriteria.add(innerCrit);
                    }
                }
            }

            // Catch the case where we had nested and/or blocks with no sub criteria so end
            // up with an empty object all the way up.
            if (newInnerCriteria.length == 0 && clearEmptyCriteria) return null;
            if (newInnerCriteria.length == 1) return newInnerCriteria[0];
            criteria.criteria = newInnerCriteria;
            return criteria;
        // anything that isn't an 'and' or 'or' can't be simplified further.
        } else {
            return criteria;
        }
    },

    //> @classMethod    DataSource.combineFieldData()  ([IA])
    //  Internal method to combine data from a field config object with an underlying DataSource
    // field definition.
    // @param   field   (object)    field to update with DS field defaults.
    // @param dsField (object) dataSource field definition
    // @return  (object)            field with its data combined with the DS data
    // @visibility internal
    //<

    // isDataSource parameter indicates we're subclassing a dataSource - there are some
    // differences between this and standard DBC attribute inheritance - for example
    // the handling of field.hidden
    combineFieldData : function (localField, dsField, isDataSource, propertiesAttr) {
        if (dsField == null) return localField;

        // If passed a "propertiesAttr", pick up any properties specified under that
        // attribute flag and apply them, while still allowing them to be overridden
        // by properties applied directly to the local component field.

       if (propertiesAttr != null && dsField[propertiesAttr] != null) {
            dsField = isc.addProperties({}, dsField, dsField[propertiesAttr]);
        }

        for (var propertyName in dsField) {
            // validators should be combined, not overridden
            if (propertyName == "validators" && localField.validators != null &&
                dsField.validators != localField.validators)
            {
                // If the parent field is using the shared, default validator set and
                // the child field is not of the same base SimpleType as the parent
                // field, do not inherit the validators.  This prevents us inheriting
                // inappropriate validators in certain common use cases (such as when
                // we have a DS that autoDerives its schema from a SQL table, and then
                // overrides certain fields to be type "boolean" rather than the
                // discovered type, which will often be "integer" because SQL has no
                // native boolean type)
                if (dsField.validators._typeValidators) {
                    if (isc.SimpleType.getBaseType(localField.type) !=
                        isc.SimpleType.getBaseType(dsField.type))
                    {
                        continue;
                    }
                }

                for (var i =0; i < dsField.validators.length; i++) {
                    var dsValidator = dsField.validators[i];
                    // We can end up with a set of validators on the parent field that is not marked
                    // with the collection-level "_typeValidators" flag because some of the
                    // validators are not typeValidators, but some are.  So exclude the individual
                    // validators from inheritance if necessary

                    if (dsValidator._typeValidator) continue;
                    //this.logWarn("comparing validators in field: " + field.name);
                    // This check is required as if 'combineFieldData' gets called more than once
                    // on the same field object (or on 2 field objects pointing to the same
                    // validators array), we can end up with duplicate validator objects.
                    if (!localField.validators.contains(dsValidator)) {
                        // if the field is using the shared, default validators for the type,
                        // make a copy before modifying
                        if (localField.validators._typeValidators) {
                            localField.validators = localField.validators.duplicate();
                        }
                        localField.validators.add(dsValidator);
                    }
                }
                continue;
            }
            // For "editorProperties"
            // - combine rather than clobbering ds-field settings
            // - also be sure to duplicate - we edit the editorProperties directly within widgets
            //   (EG applying change handlers to them, etc) and don't want to pollute the originals
            //   stored on the DataSource
            if (!this.dontDuplicateEditorProperties) {
                if (propertyName == "editorProperties") {
                    localField.editorProperties = isc.addProperties({}, dsField.editorProperties);
                    continue;
                }
            }

            // datasource properties act as defaults - they don't override
            if (localField[propertyName] != null) continue;

            // ignore 'name' field - the component-field may have retrieved this ds field via
            // dataPath but we don't want to write our name onto the component-level field
            // definition
            if (propertyName == "name") continue;

            // Ignore "hidden" property - this has slightly different meaning at the
            // component level (allows you to default showIf to "return false;")
            if (!isDataSource && propertyName == "hidden") continue;

            // copy any properties the field does not have a value for
            localField[propertyName] = dsField[propertyName];
        }
        return localField;
    },

    // Record level summary functions


    //> @type RecordSummaryFunction
    // Function to produce a summary value based on field values within a record.
    // Example usage is record-level summaries in
    // +link{listGridFieldType,"summary" type ListGrid fields}.
    // <P>
    // SummaryFunctions may be specified in one of 2 ways:<ul>
    // <li>as an explicit function or executable
    // +link{group:stringMethods,StringMethod}. Takes 3 parameters:
    // <br><code>record</code> the record for which the value is being calculated
    // <br><code>fields</code> Array of listGridFields from which summaries should be calculated
    // <br><code>summaryField</code> pointer to the summary type field on which the summary function
    // is being run.</li>
    // <li>As a registered RecordSummaryFunction identifier</li></ul>
    // Note that +link{DataSource.registerRecordSummaryFunction()} may be used to expand the
    // set of registered RecordSummaryFunctions.
    //
    // @value sum Iterates through each field, picking up the numeric field values from the record
    // and summing them.
    // @value avg Iterates through each field, picking up the numeric field values from the record
    // and calculating the mean value.
    // @value max Iterates through each field, picking up the numeric field values from the record
    // and calculating the maximum value.
    // @value min Iterates through each field, picking up the numeric field values from the record
    // and calculating the minimum value.
    // @value multiplier Iterates through each field, picking up the numeric field values from the
    // record and multiplying them together.
    // @visibility external
    //<

    // These are documented under type RecordSummaryFunction
    _recordSummaryFunctions:{
        sum : function (record, fields, summaryField) {
            var sumValue = 0;
            for (var i = 0; i < fields.length; i++) {
                var value = record[fields[i].name],
                    floatValue = parseFloat(value);
                if (isc.isA.Number(floatValue)  && floatValue == value) {
                    sumValue += floatValue;
                } else {
                    // just ignore null / empty values
                    if (value != null && value != isc.emptyString) {
                        // returning null indicates invalid value
                        return null;
                    }
                }
            }
            return sumValue;
        },

        avg : function (record, fields, summaryField) {
            var total = 0, count=0;
            for (var i = 0; i < fields.length; i++) {
                var value = record[fields[i].name],
                    floatVal = parseFloat(value);
                if (isc.isA.Number(floatVal) && (floatVal == value)) {
                    count += 1;
                    total += floatVal;
                } else {
                    if (value != null && value != isc.emptyString) {
                        // returning null indicates invalid value
                        return null;
                    }
                }
            }
            return count > 0 ? total/count : null;
        },

        max : function (record, fields, summaryField) {
            var max, isNumber;
            for (var i = 0; i < fields.length; i++) {
                var value = record[fields[i].name];
                if (isc.isA.Date(value)) {
                    if (isNumber) return null;
                    if (max == null) max = value.duplicate();
                    else if (max.getTime() < value.getTime()) max = value.duplicate();
                } else {
                    isNumber = true;
                    var floatVal = parseFloat(value);
                    if (isc.isA.Number(floatVal) && (floatVal == value)) {
                        if (max == null) max = floatVal;
                        else if (max < value) max = floatVal;
                    } else {
                        if (value != null && value != isc.emptyString) {
                            // returning null indicates invalid value
                            return null;
                        }
                    }
                }
            }
            return max;
        },
        min : function (record, fields, summaryField) {
            var min, isNumber
            for (var i = 0; i < fields.length; i++) {
                var value = record[fields[i].name];
                if (isc.isA.Date(value)) {
                    if (isNumber) return null;
                    if (min == null) min = value.duplicate();
                    if (value.getTime() < min.getTime()) min = value.duplicate();
                } else {
                    // ignore empty values
                    if (value == null || value == isc.emptyString) continue;

                    isNumber = true;
                    var floatVal = parseFloat(value);
                    if (isc.isA.Number(floatVal) && (floatVal == value)) {
                        if (min == null) min = floatVal;
                        else if (min > value) min = floatVal;
                    } else {
                        // returning null indicates invalid value
                        return null;
                    }
                }
            }
            return min;
        },
        multiplier : function (record, fields, summaryField) {
            var multiplier = 0;
            for (var i = 0; i < fields.length; i++) {
                var value = record[fields[i].name],
                    floatVal = parseFloat(value);
                if (isc.isA.Number(floatVal) && (floatVal == value)) {
                    if (i == 0) multiplier = floatVal;
                    else multiplier = (multiplier * floatVal);
                } else {
                    // returning null indicates invalid value
                    return null;
                }
            }
            return multiplier;
        }

    },

    //> @classMethod DataSource.applyRecordSummaryFunction()
    // Applies a +link{type:RecordSummaryFunction} to a record and returns the result.
    // @param summaryFunction (SummaryFunction) Summary Function or identifier for
    //   registered recordSummaryFunction to execute. If passed in as an explicit function
    //   record, fields and summaryField parameters will be passed through to the function.
    // @param record (DataSourceRecord) Record to retrieve a summary for
    // @param fields (Array of DataSourceFields) Set of fields to include in the summary
    // @param summaryField (DataSourceField) field in which this summary will be displayed.
    // @return (any) summary value for the record
    // @visibility external
    //<
    applyRecordSummaryFunction : function (summaryFunction,record,fields,summaryField) {
        if (!record || !fields) return;
        if (isc.isA.String(summaryFunction)) {
            if (this._recordSummaryFunctions[summaryFunction]) {
                summaryFunction = this._recordSummaryFunctions[summaryFunction]
            } else {
                summaryFunction = isc.Func.expressionToFunction("record,fields,summaryField",
                                                                summaryFunction);
            }
        }
        if (isc.isA.Function(summaryFunction)) return summaryFunction(record,fields,summaryField);
    },

    //> @classMethod DataSource.registerRecordSummaryFunction()
    // Register a new standard +link{type:RecordSummaryFunction}. This will then be available
    // by calling +link{SimpleType.applySummaryFunction(),applySummaryFunction()} and passing in just the new method name.
    // @param methodName (string) identifier for the new summary function
    // @param summaryFunction (function | stringMethod) new summary function implementation.
    //  This method should take 3 parameters: <code>record</code> (the record for which the
    //  summary is being generated), <code>fields</code> (an array of fields to include in the
    //  generated summary) and <code>summaryField</code> (a pointer to the field in which the
    //  summary will be displayed [may be null].
    // @visibility external
    //<
    registerRecordSummaryFunction : function (methodName, summaryFunction) {
        if (isc.isA.String(summaryFunction)) {
            summaryFunction = isc.Func.expressionToFunction("record,fields,summaryField",
                                                            summaryFunction);
        }
        this._recordSummaryFunctions[methodName] = summaryFunction;
    },



    exportFormatToFileExtensionMap: {
        xml: "xml",
        json: "json",
        csv: "csv",
        xls: "xls",
        ooxml: "xlsx"
    },
    addExportFilenameExtension : function (fileName, exportFormat) {
        if (!exportFormat) exportFormat = "csv";

        if (!this._filenameExtensions) {
            this._filenameExtensions = isc.getValues(this.exportFormatToFileExtensionMap);
        }

        var extensionForFormat = this.exportFormatToFileExtensionMap[exportFormat];
        // don't modify the filename if it's not a known format
        if (!extensionForFormat) return fileName;

        var lastDotIndex = fileName.lastIndexOf(".");
        // no extension on filename - add
        if (lastDotIndex == -1) {
            fileName = fileName+"."+extensionForFormat;
        } else {
            // appears to have an extension - check against our known extensions
            var userSpecifiedExtension = fileName.substring(lastDotIndex+1).toLowerCase();
            if (userSpecifiedExtension != extensionForFormat) {
                // user specified extension does not match exportFormat - if it matches one of
                // our known export format extensions, warn and correct
                if (this._filenameExtensions.contains(userSpecifiedExtension)) {
                    var newFileName = fileName.substring(0, lastDotIndex+1)+extensionForFormat;
                    this.logWarn("Extension specified in fileName: " + fileName
                        + " is incorrect for exportFormat: '"
                        + exportFormat+"'."+" Changing fileName to: " + newFileName);
                    fileName = newFileName;
                } else {
                    // assume the user specified extension is not an extension, but is part of
                    // the filename and that we therefore need to add an extension
                    fileName = fileName+"."+extensionForFormat;
                }
            }
        }
        return fileName;
    },
    //> @classMethod dataSource.exportClientData()
    // Exports arbitrary client-side data, with client-side formatters applied, so is suitable
    // for direct display to users.  This method can be used to export data formatted outside
    // of any kind of visual component.
    // <P>
    // Requires the SmartClient server, but does not rely on any server-side DataSources.  If
    // you need to intervene in the export process server-side - for example, if you need to
    // do something not directly supported with the exported object, such as attach it to an
    // email - use the +link{method:dataSource.exportClientData,instance method} with an
    // appropriate +link{class:OperationBinding}, as described in the method documentation.
    // <P>
    // To export unformatted data, see +link{dataSource.exportData, exportData} which does
    // not include client-side formatters, but requires both the SmartClient server and the
    // presence of server-side DataSources.
    // <P>
    // Note that field +link{DataSourceField.displayFormat,displayFormat} is honored for
    // "date" and "datetime" fields when exporting direct to Excel; see the displayFormat
    // docs for details.
    //
    // @param data (Array of Record) Array of Records to export
    // @param requestProperties (DSRequest properties) Request properties for the export
    // @param callback (DSCallback) Optional callback.  Note that this is only applicable
    // if you also specify +link{DSRequest.exportToClient,exportToClient}: false in the
    // request properties
    // @visibility external
    //<

    exportClientData : function (data, requestProperties, callback, ds) {

        requestProperties = isc.DataSource.dupRequest(requestProperties);

        var props = requestProperties.exportContext || requestProperties || {},
            format = props && props.exportAs ? props.exportAs : "csv",
            exportDisplay = props && props.exportDisplay ? props.exportDisplay : "download",
            downloadToNewWindow = props.downloadToNewWindow != null ?
                                    props.downloadToNewWindow : (exportDisplay == "window")
        ;

        var exportAs = format;
        if (requestProperties.operationId != null) {
            var operationBinding = ds.getOperationBinding("clientExport", requestProperties.operationId);
            if (operationBinding != null && operationBinding.exportAs) {
                exportAs = operationBinding.exportAs;
            }
        }
        var fileName = this.addExportFilenameExtension(props && props.exportFilename ? props.exportFilename : "export",
                                                       exportAs);

        var serverProps = {
            showPrompt:false,
            transport: props.exportToClient === false ? "xmlHttpRequest" : "hiddenFrame",
            exportResults: true,
            exportDisplay: exportDisplay,
            targetMainWindow: props.targetMainWindow,
            downloadResult: !(props.exportToClient === false),
            downloadToNewWindow: downloadToNewWindow,
            download_filename: (exportDisplay == "window" ? fileName : null),
            params:props.params
        };

        var settings = {
            exportAs: format,
            exportDisplay: exportDisplay,
            exportFilename: fileName,
            exportPath: props.exportPath,
            exportToClient: props.exportToClient,
            exportToFilesystem: props.exportToFilesystem,
            exportDelimiter: props.exportDelimiter,
            exportFields: props.exportFields,
            exportHeader: props.exportHeader,
            exportHeaderless: props.exportHeaderless,
            exportFooter: props.exportFooter,
            exportTitleSeparatorChar: props.exportTitleSeparatorChar,
            exportDefaultBGColor: props.exportDefaultBGColor,
            exportAlternateRowBGColor: props.exportAlternateRowBGColor,
            exportRowBGColors: props.exportRowBGColors,
            exportColumnBGColors: props.exportColumnBGColors,
            exportRawValues: props.exportRawValues,
            exportCurrencySymbol: isc.NumberUtil.currencySymbol,
            lineBreakStyle: props.lineBreakStyle
        };

        if (props.headerSpans) settings.headerSpans = props.headerSpans;
        if (props.exportHeaderSpans) settings.exportHeaderSpans = props.exportHeaderSpans;
        if (props.exportOtherFields) settings.exportOtherFields = props.exportOtherFields;
        if (props.exportShowHeaderSpanTitles != null) {
            settings.exportShowHeaderSpanTitles = props.exportShowHeaderSpanTitles;
        }
        if (props.exportSpanTitleSeparator != null) {
            settings.exportSpanTitleSeparator = props.exportSpanTitleSeparator;
        }
        if (props.formulaFields) settings.formulaFields = props.formulaFields;
        if (props.formulaRemap) settings.formulaRemap = props.formulaRemap;
        if (props.exportHeaderHeight != null) {
            settings.exportHeaderHeight = props.exportHeaderHeight;
        }
        if (props.exportFieldPixelWidths) {
            settings.exportFieldPixelWidths = props.exportFieldPixelWidths;
            settings.exportWidthScale = props.exportWidthScale;
        }

        settings.exportWrapHeaderTitles = props.exportWrapHeaderTitles;

        if (props.exportAlignments) {
            settings.exportAlignments = props.exportAlignments;
        }

        var opId = requestProperties.operationId;
        if (ds == null || opId == null) {
            isc.DMI.callBuiltin({
                methodName: "downloadClientExport",
                arguments: [ data, format, fileName, exportDisplay, settings ],
                requestParams: serverProps,
                callback: callback
            });
        } else {
            isc.addProperties(serverProps, {parameters: settings});
            ds.performClientExportOperation(opId, data, callback, serverProps);
        }
    },

    // Helper method for validation error manipulation:

    //> @classMethod DataSource.getSimpleErrors()
    // Getter method for extracting server-side validation errors for an attempted
    // "update" or "add" operation, as a  JS Object where each property name is a
    // field name from the record and each property value is an array of error message strings.
    // For example:<pre>
    //     {
    //         userId : ["A user with this userId already exists"],
    //         orderId : ["Must be a numeric value", "No Order with ID '6A18294' exists"]
    //     }
    // </pre>
    // The Java API DSResponse.addError(fieldName, errorMessage) is used to send server-side errors
    // to the client.  See the Java Server Reference for details.
    //
    // @param dsResponse (DSResponse) response from which you want to extract the errors
    // @return (Object) Map of fieldName to error messages for the errors included in the DSResponse.
    // @group errorHandling
    // @visibility external
    //<
    getSimpleErrors : function (dsResponse) {
        var complexErrors = dsResponse ? dsResponse.errors : null;
        if (isc.isAn.Array(complexErrors)) {
            if (complexErrors.length > 1) {
                this.logWarn("getSimpleErrors() passed an array of error blocks - " +
                    "dropping all but the first set of errors.");
            }
            complexErrors = complexErrors[0]
        }
        if (complexErrors == null) return null;

        // The server hands us errors in the format
        // { recordPath:pathString, fieldName:[{errorMessage:"foo", resultingValue:"moo"}] }
        // Simplify this down to just
        // { fieldName:["error message 1", "error message 2"] }

        var simpleErrors = {};
        for (var fieldName in complexErrors) {
            var fieldErrors = complexErrors[fieldName];
            if (fieldName == "recordPath" && !isc.isAn.Object(fieldErrors)) continue;

            var errorStrings = [];

            if (!isc.isAn.Array(fieldErrors)) fieldErrors = [fieldErrors];

            for(var i = 0; i < fieldErrors.length; i++) {
                var error = fieldErrors[i];
                if (isc.isAn.Object(error)) {
                    errorStrings.add(error.errorMessage);
                // handle it already being a raw string
                } else if (error != null) {
                    errorStrings.add(error);
                }
            }
            if (errorStrings.length > 0) {
                simpleErrors[fieldName] = errorStrings;
            }
        }
        return simpleErrors;
    },

    recordTimingData : function(object, description, type, millis) {
        if (!isc.Log.logIsInfoEnabled("RpcTabTiming") || !object) return;
        if (millis == null) millis = isc.timeStamp();
        var timing;
        if (object.clientTimingStack) {
            timing = object.clientTimingStack[object.clientTimingStack.length-1];   //Peek
        } else {
            timing = object.clientTiming = {name: "Client processing", start: millis, children: []};
            object.clientTimingStack = [];
            object.clientTimingStack.add(object.clientTiming);  // Push
        }
        if (!timing) {
            isc.logWarn("Unexpected failure to find stack entry logging timing data: " +
                                "description: '" + description + "', type: " + type);
            return;
        }
        if (type == "start") {
            var work = {
                start: millis,
                name: description,
                children: []
            };
            timing.children.add(work);
            object.clientTimingStack.add(work);  //Push
        } else if (type == "end") {
            timing.end = millis;
            object.clientTimingStack.removeAt(object.clientTimingStack.length-1);  //Pop
        } else {
            timing.children.add({
                millis: millis,
                name: description
            });
        }

    }

});


isc.DataSource.addMethods({

    //> @method dataSource.supportsAdvancedCriteria()
    // Do fetch and filter operations on this dataSource support being passed
    // +link{AdvancedCriteria}?
    // <P>
    // For a DataSource to support being passed AdvancedCriteria, it must be
    // +link{clientOnly,clientOnly:true} or +link{cacheAllData,cacheAllData:true}, or have
    // server side logic which can process AdvancedCriteria objects passed from the client.
    // <P>
    // AdvancedCriteria are supported on the server for standard
    // +link{group:sqlDataSource,SQL}, +link{group:hibernateIntegration,Hibernate} and
    // +link{group:jpaIntegration,JPA} DataSources in SmartClient Enterprise or Power editions
    // (not supported in SmartClient Pro).
    // <P>
    // The framework assumes that custom dataSources support AdvancedCriteria; if you have a
    // a custom DataSOurce implementation that does not support AdvancedCriteria, you can set
    // the +link{allowAdvancedCriteria} property to false.
    //
    // @return (Boolean) true if this dataSource supports being passed AdvancedCriteria in fetch
    //  and filter type operations, false otherwise.
    // @visibility external
    //<
    supportsAdvancedCriteria : function () {
        if (this.clientOnly || this.cacheData) return true;
        if (this.allowAdvancedCriteria === false) return false;
        if (this.willHandleAdvancedCriteria === false) return false;

        return true;
    },

    //> @attr dataSource.allowAdvancedCriteria (boolean : null : IRWA)
    // By default, all DataSources are assumed to be capable of handling
    // +link{object:AdvancedCriteria} on fetch or filter type operations.  This property may be
    // set to <code>false</code> to indicate that this dataSource does not support
    // advancedCriteria. See +link{supportsAdvancedCriteria()} for further information on this.
    // <p>
    // <b>NOTE:</b> If you specify this property in a DataSource descriptor
    // (<code>.ds.xml</code> file), it is enforced on the server.  This means that if you run
    // a request containing AdvancedCriteria against a DataSource that advertises itself as
    // <code>allowAdvancedCriteria:false</code>, it will be rejected.
    // @see operationBinding.allowAdvancedCriteria
    // @serverDS allowed
    // @visibility external
    //<


    isAdvancedCriteria : function (criteria) {
        return isc.DS.isAdvancedCriteria(criteria, this);
    },

    //> @method dataSource.normalizeAdvancedCriteria()
    // This method returns a normalized form of an AdvancedCriteria (it does not affect the
    // original object).  Normalization consists of identifying subcriteria that specify a
    // list of values against an operator that only deals with  single values, and expanding
    // such a subcriterion into a new "or" subclause.  For example, the criterion:<pre>
    //   { fieldName: "state", operator: "iEquals", value: ["NY", "il", "ca", "Tx"] }
    // </pre>
    // will be expanded into this subclause, which will replace it in the normalized
    // criteria:<pre>
    //   { operator: "or", criteria: [
    //       { fieldName: "state", operator: "iEquals", value: "NY" },
    //       { fieldName: "state", operator: "iEquals", value: "il" },
    //       { fieldName: "state", operator: "iEquals", value: "ca" },
    //       { fieldName: "state", operator: "iEquals", value: "Tx" }
    //     ]
    //   }
    // </pre>
    // For subcriteria that are of a negated type, such as "iNotEqual", we use a new "and"
    // clause rather than an "or" clause.  This is because if I say:<pre>
    //       { fieldName: "state", operator: "notEqual", value: ["NY", "IL", "CA"] }
    // <pre>
    // I mean that I want all of the states except those three: so logically, not NY <b>and</b>
    // not IL <b>and</b> not CA.
    // <p>
    // Note that normalization is only carried out for those operators where it makes sense to
    // do so.  For example, checking if a field <code>equals</code> "A" or "B" or "C", is
    // clearly a sensible and coherent thing to do.  Equally clearly, checking if a field is
    // <code>greaterThan</code> 12 or 6 or 9 does not make any sense.  We decide whether to
    // apply normalization based upon the operator's <code>canNormalize</code> setting.  The
    // full list of built-in operators which have this flag set is as follows:
    // <ul>"equals", and its variants "iEquals", "notEqual" and "iNotEqual"</ul>
    // <ul>"contains" and its variants</ul>
    // <ul>"startsWith" and its variants</ul>
    // <ul>"endsWith" and its variants</ul>
    // <ul>"equalsField" and "notEqualField"</ul>
    // <ul>"iEqualsField" and "iNotEqualField"</ul>
    // <ul>"containsField" and its variants</ul>
    // <ul>"startsWithField" and its variants</ul>
    // <ul>"endsWithField" and its variants</ul>
    //
    // @param criteria (AdvancedCritiera) The AdvancedCriteria to normalize
    // @return (AdvancedCriteria) The normalized AdvancedCriteria object (the original object
    //                            is not changed)
    // @visibility internal for now
    //<
    normalizeAdvancedCriteria : function (criteria, subCrit) {
        var norm = {};
        if (!subCrit) {
            norm._constructor = "AdvancedCriteria";
            if (criteria.strictSQLFiltering) norm.strictSQLFiltering = true;
        }
        if (criteria.criteria) {
            // don't break on a single element rather than an array in the criteria slot
            if (!isc.isAn.Array(criteria.criteria)) {
                criteria.criteria = [criteria.criteria];
            }
            norm.operator = criteria.operator;
            norm.criteria = [];
            for (var i = 0; i < criteria.criteria.length; i++) {
                norm.criteria.add(this.normalizeAdvancedCriteria(criteria.criteria[i], true));
            }
        } else {
            if (!isc.isAn.Array(criteria.value)) {
                isc.addProperties(norm, criteria);
            } else {
                var operator = this.getSearchOperator(criteria.operator);
                if (!operator || !operator.canNormalize) {
                    isc.addProperties(norm, criteria);
                } else {
                    norm.operator = operator.negate ? "and" : "or";
                    norm.criteria = [];
                    for (var i = 0; i < criteria.value.length; i++) {
                        norm.criteria.add({
                            fieldName: criteria.fieldName,
                            operator: criteria.operator,
                            value: criteria.value[i]
                        });
                    }
                }
            }
        }
        // For a criterion with fieldName and value but no operator,
        // default the operator to "equals".
        if (!norm.operator && norm.fieldName && norm.value) norm.operator = "equals";

        return norm;
    },

    //> @method dataSource.addSearchOperator()
    // Add a new search operator, only to this DataSource.
    // <P>
    // If an existing +link{Operator} is passed, restricts the set of FieldTypes to which that
    // operator can be applied in this DataSource.
    // <P>
    // See also +link{classMethod:DataSource.addSearchOperator()} for adding operators to all
    // DataSources.
    //
    // @param operator (Operator) definition of the operator to add
    // @param [types] (Array of FieldType) types to which this operator applies
    // @group advancedFilter
    // @visibility external
    //<
    addSearchOperator : function (operator, types) {
        if (!operator || !operator.ID) {
            isc.logWarn("Attempted to add null search operator, or operator with no ID");
            return;
        }

        // Register the "new" operator with the class, regardless of whether it's already
        // there. If it already exists, it will be replaced, which is the behavior we want.
        isc.DataSource.addSearchOperator(operator);

        // create an object for DS instance-level typeOperators
        if (!this._typeOperators ) this._typeOperators = {};
        // when added via addSearchOperator(), getTypeOperators() returns them along with the
        // list of global ones - when ops are added via setTypeOperators(), getTypeOperators()
        // returns only the specified ops for those types
        if (!this._typeOperators._includeGlobals) this._typeOperators._includeGlobals = {};
        if (types) {
            // First we need to remove it from whatever it's attached to right now
            for (var idx = 0; idx < this._typeOperators.length; idx++) {
                this._typeOperators[idx].remove(operator.ID);
            }
            // And now attach it to the types we've been passed
            for (var idx = 0; idx < types.length; idx++) {
                if (!this._typeOperators[types[idx]]) {
                    this._typeOperators[types[idx]] = [ operator.ID ];
                }
                if (!this._typeOperators[types[idx]].contains(operator.ID)) {
                    this._typeOperators[types[idx]].add(operator.ID);
                }
                this._typeOperators._includeGlobals[types[idx]] = true;
            }
        } else {
            if (!this._typeOperators["_all_"]) {
                this._typeOperators["_all_"] = [ operator.ID];
            }
            if (!this._typeOperators["_all_"].contains(operator.ID)) {
                this._typeOperators["_all_"].add(operator.ID);
            }
        }
    },


    //> @method dataSource.getSearchOperator()
    // Get the +link{Operator} definition for an +link{OperatorId}.
    //
    // @param operatorId (OperatorId) the id of the operator
    // @return (Operator) the +link{Operator} definition
    // @group advancedFilter
    // @visibility external
    //<
    getSearchOperator : function (operatorId) {
        return isc.DataSource._operators[operatorId];
    },

    //> @method dataSource.getTypeOperators()
    // Get the list of +link{OperatorId}s available on this DataSource for the given +link{FieldType}.
    // <P>
    // If +link{setTypeOperators()} has been called for this DataSource and FieldType, returns that
    // list, otherwise, returns the set of valid operators for the +link{FieldType} as specified by
    // +link{simpleType.validOperators}, otherwise, the system-wide set of valid operators for the
    // type as registered via +link{classMethod:DataSource.addSearchOperator()}.
    //
    // @param [typeName] (FieldType | String) Defaults to "text" if not passed.
    // @return (Array of OperatorId) available Operators
    // @group advancedFilter
    // @visibility external
    //<
    getTypeOperators : function (typeName) {
        typeName = typeName || "text";

        var typeOps = [],
            type = isc.SimpleType.getType(typeName),
            dsTypes = this._typeOperators
        ;
        if (dsTypes) {

            while (type && !dsTypes[type.name]) {
                if (!type || !type.inheritsFrom) break;
                type = isc.SimpleType.getType(type.inheritsFrom, this);
            }
            if (type) {
                var globals = dsTypes._includeGlobals,
                    includeGlobals = globals && globals[type.name] != false
                ;
                if (dsTypes[type.name]) {
                    // add any type-specific operators
                    typeOps.addList(dsTypes[type.name]);
                }
                // add any operators specified for type "_all_"
                typeOps.addList(dsTypes["_all_"]);


                if (!includeGlobals && typeOps.length > 0) {
                    if (type.validOperators) {
                        // if the simpleType has validOperators, restrict the list
                        typeOps = typeOps.intersect(type.validOperators);
                    }
                    return typeOps;
                }
            }
        }

        dsTypes = isc.DataSource._typeOperators;
        // look up the global list of operators for this type
        type = isc.SimpleType.getType(typeName);
        while (type && !dsTypes[type.name]) {
            if (!type || !type.inheritsFrom) break;
            type = isc.SimpleType.getType(type.inheritsFrom, this);
        }

        if (type && dsTypes[type.name]) {
            // add type-specific operators
            typeOps.addList(dsTypes[type.name]);
        }
        // add any operators specified for type "_all_"
        typeOps.addList(dsTypes["_all_"]);

        if (type && type.validOperators) {
            // if the simpleType has validOperators, restrict the list
            typeOps = typeOps.intersect(type.validOperators);
        }

        typeOps = typeOps.getUniqueItems();

        return typeOps;
    },

    //> @method dataSource.setTypeOperators()
    // Set the list of +link{OperatorId}s valid for a given FieldType.
    //
    // @param typeName (FieldType | String)
    // @param operators (Array[] of OperatorId) available Operators
    // @group advancedFilter
    // @visibility external
    //<
    setTypeOperators : function (typeName, operators) {
        if (!operators) return;
        if (!isc.isAn.Array(operators)) operators = [ operators ];
        if (!this._typeOperators) this._typeOperators = {};
        this._typeOperators[typeName || "_all_"] = operators;
        if (!this._typeOperators._includeGlobals) this._typeOperators._includeGlobals = {};
        this._typeOperators._includeGlobals[typeName] = false;
    },

    //> @method dataSource.getFieldOperators()
    // Get the list of +link{OperatorId}s available for this field.
    // <P>
    // By default, if +link{dataSourceField.validOperators,field.validOperators} is set,
    // returns that list, otherwise returns the result of +link{dataSource.getTypeOperators()}.
    // @param field (String or DataSourceField) Field (or field name) to obtain operators for
    // @return (Array of OperatorId) available Operators
    // @group advancedFilter
    // @visibility external
    //<
    getFieldOperators : function (field) {
        if (isc.isA.String(field)) field = this.getField(field);
        if (!field) return [];

        if (field && field.validOperators) return field.validOperators;
        var baseFieldType = isc.SimpleType.getType(field.type);
        var type = field.type || "text";
        if (!baseFieldType) type = "text";
        return this.getTypeOperators(type);
    },

    //> @method dataSource.getFieldOperatorMap()
    // Get the list of +link{Operator}s available for this field, as a +link{ValueMap} from
    // +link{OperatorId} to the +link{operator.title} specified for the +link{Operator}, or
    // the corresponding property in +link{Operators} if +link{operator.titleProperty} is set.
    // <P>
    // This valueMap is suitable for use in a UI for building queries, similar to the
    // +link{FilterBuilder}, and optionally omits operators marked +link{operator.hidden}:true.
    // <p>
    // It is also possible to have this function return only operators of a given
    // +link{OperatorValueType}, or everything except operators of that type. This is useful,
    // for example, if you want to return all the logical operators (like "and"), or everything
    // except the logical operators.
    //
    // @param field (string or DataSourceField) Field (or field name) to obtain operator map for.
    // @param [includeHidden] (boolean) whether to include Operators marked hidden:true
    // @param [valueType] (OperatorValueType) If passed, returns only operators of this +link{OperatorValueType}
    // @param [omitValueType] (boolean) If set, reverses the meaning of the <code>valueType</code>
    //        parameter, so operators of that +link{OperatorValueType} are the only ones omitted
    // @return (ValueMap) mapping from +link{OperatorId} to title, as described above
    // @group advancedFilter
    // @see dataSource.getTypeOperatorMap
    // @visibility external
    //<
    getFieldOperatorMap : function (field, includeHidden, valueType, omitValueType, operators) {
        if (isc.isA.String(field)) field = this.getField(field);
        var valueMap = {},
            operators = operators || this.getFieldOperators(field);
        includeHidden = includeHidden || field.validOperators != null;

        for (var idx = 0; idx < operators.length; idx++) {
            var operatorId = operators[idx];
            var searchOp = this.getSearchOperator(operatorId);

            if (!searchOp) continue; // no such operatorId

            // searchOp is marked hidden, and we're not including hidden operators
            if (searchOp.hidden && !includeHidden &&
                // and the operator hasn't been explicitly specified on the field (which
                // overrides the default "hidden" property)
                (!isc.isAn.Array(field.validOperators) ||
                 !field.validators.contains(operatorId))) continue;

            // check that the operator is appropriate for the valueType requested (if any)
            if (!valueType || (searchOp.valueType == valueType) == !omitValueType) {
                valueMap[operators[idx]] = searchOp.titleProperty == null ? searchOp.title :
                    isc.Operators[searchOp.titleProperty];
            }
        }
        return valueMap;
    },

    //> @method dataSource.getTypeOperatorMap()
    // Get the list of +link{Operator}s available for this +link{FieldType}, as a +link{ValueMap} from
    // +link{OperatorId} to the +link{operator.title} specified for the +link{Operator}, or
    // the corresponding property in +link{Operators} if +link{operator.titleProperty} is set.
    // <P>
    // This valueMap is suitable for use in a UI for building queries, similar to the
    // +link{FilterBuilder}, and optionally omits operators marked +link{operator.hidden}:true.
    // <p>
    // It is also possible to have this function return only operators of a given
    // +link{OperatorValueType}, or everything except operators of that type. This is useful,
    // for example, if you want to return all the logical operators (like "and"), or everything
    // except the logical operators.
    //
    // @param [type] (FieldType) Type to obtain operator map for. Defaults to "text" if not passed.
    // @param [includeHidden] (boolean) whether to include Operators marked hidden:true
    // @param [valueType] (OperatorValueType) If passed, returns only operators of this +link{OperatorValueType}
    // @param [omitValueType] (boolean) If set, reverses the meaning of the <code>valueType</code>
    //        parameter, so operators of that +link{OperatorValueType} are the only ones omitted
    // @return (ValueMap) mapping from +link{OperatorId} to title, as described above
    // @group advancedFilter
    // @see dataSource.getFieldOperatorMap
    // @visibility external
    //<
    getTypeOperatorMap : function (type, includeHidden, valueType, omitValueType) {
        var valueMap = {},
            operators = this.getTypeOperators(type);
        for (var idx = 0; idx < operators.length; idx++) {
            var searchOp = this.getSearchOperator(operators[idx]);
            if (searchOp && (!searchOp.hidden || includeHidden)) {
                if (!valueType || (searchOp.valueType == valueType) == !omitValueType)
                valueMap[operators[idx]] = searchOp.titleProperty == null ? searchOp.title :
                    isc.Operators[searchOp.titleProperty];
            }
        }
        return valueMap;
    },

    //> @method dataSource.recordsAsText()
    // Converts a list of Records to simple text formats with a Record per line and values separated
    // by a configurable separator, including both tab-separated-values and comma-separated-values
    // (aka CSV).
    // <P>
    // In addition to the <code>settings</code> parameter for this method,
    // +link{dataSourceField.exportForceText} can be set.
    // <P>
    // If two or more different text exports are needed for the same DataSource creating a conflict
    // for any DataSourceField setting, +link{DataSource.inheritsFrom} can be used to create a
    // child DataSource where these settings can be changed without recapitulating all field
    // definitions.
    //
    // @param records (Array of Record) records to convert
    // @param [settings] (TextExportSettings Properties) settings for the export
    // @return (String) records as CSV/TSV (separator can be specified)
    // @visibility external
    //<
    recordsAsText : function (records, settings) {
        if (!isc.isA.TextExportSettings(settings)) {
            settings = isc.TextExportSettings.create(settings);
        }

        var i, field;
        var fieldsToExport = [];

        var fieldNameList = settings.fieldList;
        if (!fieldNameList) fieldNameList = this.getFieldNames();

        for (i = 0; i < fieldNameList.length; i++) {
            var name = fieldNameList[i];
            field = this.fields[name];
            if (field) fieldsToExport.add(field);
            else fieldsToExport.add(name);
        }
        var result = "";
        for (i = 0; i < records.length; i++) {
            if (i > 0) result += settings.lineSeparator;
            result += this._recordAsText(records[i], fieldsToExport, settings);
        }
        return result;
    },

    _recordAsText : function (record, fields, settings) {
        var value, valueType;

        var forceText = settings.forceText;
        var escapeChar = settings.getEscapingModeEscapeChar();

        for (var i = 0, result = ""; i < fields.length; i++, result += value) {
            if (i > 0) result += settings.fieldSeparator;

            var field = fields[i];
            if (isc.isA.String(field)) {
                value = record[field];
                valueType = "text";
            } else {
                valueType = field.type;
                value = record[field.name];
                if (field.exportForceText) {
                    forceText = field.exportForceText;
                }
            }
            var mappedToString = false;
            if (settings.useDisplayValue && field.name != null) {
                value = this.getDisplayValue(field.name, value);
                if (value != null) {
                    value = value.toString(); mappedToString = true;
                }
            }
            if (!mappedToString) {
                value = isc.DataSource._getTypedValueAsString(value, valueType, settings);
            }
            if (valueType == "text") switch (forceText) {
            case "leadingSpace":
                value = " " + value;
                break;
            case "formula":
                value = "=" + value;
            }
            value = value.replace(/(\")/g, escapeChar + "$1");
            if (settings.quoteValues) value = "\"" + value + "\"";
        }
        return result;
    },

    //> @method dataSource.recordsFromText()
    // Derive a list of Records from Microsoft Excel-compatible tab-separated-values format, using
    // the current DataSource field order, or an explicitly specified list of fields.
    // <P>
    // If a specified field does not exist in the DataSource, it's assumed the values for that
    // field should end up as Strings.
    //
    // @param text (String) records as CSV/TSV (separator can be specified)
    // @param [settings] (TextImportSettings Properties) optional settings for the import
    // @return (Array of Record) records derived from TSV
    // @visibility external
    //<
    recordsFromText : function (text, settings) {

        if (!text) return [];

        if (!isc.isA.TextImportSettings(settings)) {
            settings = isc.TextImportSettings.create(settings);
        }

        text = settings.addFinalLineSeparatorIfNotPresent(text);

        var fieldNameList = settings.fieldList;
        if (!fieldNameList) fieldNameList = this.getFieldNames();

        if (settings.hasHeaderLine) {
            fieldNameList = [];
            text = this._processAndRemoveHeaderLine(text, settings, fieldNameList);
        }
        if (fieldNameList.length == 0) return [];

        var dataSource = this, records = [];

        var record = {}, nFields = 0;

        var fieldFunction = function (value) {
            if (nFields < fieldNameList.length) {
                record[fieldNameList[nFields++]] = value;
            }
        };
        var lineFunction = function () {
            if (!isc.TextImportSettings._importAsJsonObjects) {
                isc.Validator._acceptExcelFormats = true;
                dataSource.validateJSONRecord(record, true);
                isc.Validator._acceptExcelFormats = false;
            }
            records.add(record);
            record = {};
            nFields = 0;
            return false;
        };
        settings.parseTextAndApplyFunctions(text, fieldFunction, lineFunction);
        return records;
    },

    _processAndRemoveHeaderLine : function (text, settings, fieldList) {

        var titleMap = {}, nameMap = {};
        if (!isc.TextImportSettings._importAsJsonObjects) {
            var fieldNames = this.getFieldNames();
            for (var i = 0; i < fieldNames.length; i++) {
                var field = this.getField(fieldNames[i]);
                if (field.name  != null)  nameMap[field.name.toUpperCase()]  = field;
                if (field.title != null) titleMap[field.title.toUpperCase()] = field;
            }
        }
        var fieldFunction = function (value) {
            var target = value.toUpperCase().replace(/^(\s)*/, "").replace(/(\s)*$/, "");
            if     (titleMap[target]) fieldList.add(titleMap[target].name);
            else if (nameMap[target]) fieldList.add(nameMap [target].name);
            else                      fieldList.add(value);
        };
        var lineFunction = function () {
            return true;
        };
        return settings.parseTextAndApplyFunctions(text, fieldFunction, lineFunction);
    }

});

//> @attr dataSourceField.validOperators (Array of OperatorId : null : IR)
// List of operators valid on this field.
// <P>
// If not specified, all operators that are valid for the field type are allowed.
// @group advancedFilter
// @serverDS allowed
// @visibility external
//<

//> @attr simpleType.validOperators (Array of OperatorId : null : IR)
// Set of search operators valid for this type.
// <P>
// If not specified, the +link{inheritsFrom,inherited} type's operators will be used, finally
// defaulting to the default operators for the basic types (eg, integer).
// @group advancedFilter
// @visibility external
//<




// Filtering
// ---------------------------------------------------------------------------------------

isc.DataSource.addMethods({

    //> @method dataSource.evaluateCriterion()
    // Evaluate the given criterion with respect to the passed record.
    // <P>
    // Typically called by the +link{operator.condition,condition} function of a custom
    // +link{Operator} to evaluate +link{criterion.criteria,sub-criteria}.
    //
    // @param record (Record) record to evaluate
    // @param criterion (Criterion) criterion to use
    // @return (boolean) whether the record meets the supplied +link{Criterion}
    // @group advancedFilter
    // @visibility external
    //<
    evaluateCriterion : function (record, criterion) {


        if (criterion.requiresServer == true) return true;

        var op = this.getSearchOperator(criterion.operator);
        if (op == null) {
            isc.logWarn("Attempted to use unknown operator " + criterion.operator);
            return false;
        }

        var checkValidOps = true;
        if (this._applyingCriteriaToList) {
            for (var i = 0; i < this._testedCriteria.length; i++) {
                if (this._testedCriteria[i] == criterion) {
                    // we've already tested this criterion (as part of this loop), so no need to test
                    // again
                    checkValidOps = false;
                    // If we determined we should just 'drop' the criteria -- IE it points to
                    // an unknown field and dropUnknownCriteria is true, just return true.
                    if (this._shouldDropCriteria[i] == true) {
                        return true;
                    }
                }
            }
        }
        if (checkValidOps) {
            var testedCriteriaIndex;
            // If we're looping through a list of records applying criteria, record the
            // criteria we've checked for validity so we don't have to re-test for each record
            // in the grid.
            if (this._applyingCriteriaToList) {
                testedCriteriaIndex = this._testedCriteria.length;
                this._testedCriteria[testedCriteriaIndex] = criterion;
            }

            if (criterion.fieldName) {
                var ds = this,
                    field = this.getField(criterion.fieldName);
                if (field == null) {
                    ds = this.getDataSourceForDataPath(criterion.fieldName);
                    field = this.getFieldForDataPath(criterion.fieldName);
                    if (ds == null) {
                        ds = this;
                    }
                }

                if (field == null) {
                    if (this.dropUnknownCriteria) {
                        this.logInfo("evaluateCriterion passed criterion for field not explicitly " +
                            "listed in this dataSource:" + criterion.fieldName +
                            " - dropping this filter","AdvancedCriteria");

                        // If applying criteria to a list, record that we should just ignore this
                        // criterion and return true regardless of the record values.
                        if (this._applyingCriteriaToList) {
                            this._shouldDropCriteria[testedCriteriaIndex] = true;
                        }
                        return true;
                    } else {
                        this.logInfo("evaluateCriterion passed criterion for field not explicitly " +
                            "listed in this dataSource:" + criterion.fieldName +
                            " - continuing with filter", "AdvancedCriteria");
                    }

                } else {
                    var validOps = ds.getFieldOperators(field.name);
                    if (!validOps.contains(op.ID)) {
                        this.logWarn("Operator " + op.ID + " is not valid for field " + criterion.fieldName +
                                    ". Continuing anyway.");
                    }
                }
            }
        }

        var field = this.getField(criterion.fieldName);
        var fieldValue = isc.DataSource.getPathValue(record, criterion.fieldName, field);
        var isDateField = field && (isc.SimpleType.inheritsFrom(field.type, "date") ||
                          isc.SimpleType.inheritsFrom(field.type, "datetime"));

        var convertToAbsoluteDateOrKeepOriginalValue = function(value) {
            if (isDateField && !isc.isA.Date(value) && isc.DateUtil.isRelativeDate(value)) {
                return isc.DateUtil.getAbsoluteDate(value);
            }

            return value;
        }

        var criterionValues = {
            value: convertToAbsoluteDateOrKeepOriginalValue(criterion.value),
            start: convertToAbsoluteDateOrKeepOriginalValue(criterion.start),
            end: convertToAbsoluteDateOrKeepOriginalValue(criterion.end)
        };

        // If this is a field comparison operator, lets add the value of the other
        // field to the criterionValues object.
        if (op.valueType === "fieldName") {
            var otherField = this.getField(criterionValues.value);
            var otherFieldValue = isc.DataSource.getPathValue(record, criterionValues.value, otherField);
            criterionValues.otherValue = convertToAbsoluteDateOrKeepOriginalValue(otherFieldValue);
        } else if (op.valueType === "criteria") {
            // If we're dealing with a criteria operator such as AND, NOT & OR, lets add the criterion
            // to the criterionValues object.
            criterionValues.criterion = criterion;
            criterionValues.record = record;
        }

        var condition = false;

        // If this is a multiple: true field, we will need to run the condition check several times
        // depending on field type.
        if (field && field.multiple) {
            if (isc.isAn.Array(fieldValue) && !isc.isAn.Array(criterionValues.otherValue)) {
                var fieldDataSource = isc.DataSource.get(field.type);

                for (var i = 0; i < fieldValue.length; i++) {
                    var fieldValueToTest = fieldValue[i];

                    // If the fieldValueToTest is an object, this probably means that the field is
                    // of DataSource type and then we need to use the primaryKey fields value.
                    if (isc.isA.Object(fieldValueToTest) && fieldDataSource) {
                        if (fieldValueToTest[fieldDataSource.getPrimaryKeyFieldName()]) {
                            fieldValueToTest = fieldValueToTest[fieldDataSource.getPrimaryKeyFieldName()];
                        } else {
                            isc.logWarn("The field '" + criterion.fieldName + "' being tested is a multiple value" +
                                "field with a DataSource type but either the DataSource does not exist" +
                                "or it does not have a primary key field. Skipping criterion.");
                            break;
                        }
                    }

                    condition = op.condition(criterion.fieldName, fieldValueToTest, criterionValues, this, isDateField);

                    if (condition) {
                        break;
                    }
                }
            } else {
                if (isc.isAn.Array(fieldValue) && isc.isAn.Array(criterionValues.otherValue) &&
                    op.ID !== "equalsField" && op.ID !== "iEqualsField" &&
                    op.ID !== "notEqualField" && op.ID !== "iNotEqualField")
                {
                    // If we're comparing fields and both values are arrays and we're not using
                    // the correct operator type, lets log a warning.
                    isc.logWarn("Only 'equalsField', 'iEqualsField', 'notEqualField' and" +
                        "'iNotEqualField' are valid operators when comparing two multiple: true" +
                        "fields. Ignoring criterion with operator '" + criterion.operator + "'.");
                    condition = true;
                } else {
                    condition = op.condition(criterion.fieldName, fieldValue, criterionValues, this, isDateField);
                }
            }
        } else {
            condition = op.condition(criterion.fieldName, fieldValue, criterionValues, this, isDateField);
        }

        // If the operator is negated, lets return a negated condition
        return (op.negate ? !condition : condition);
    },

    recordsMatchingAdvancedFilter : function (data, criteria, requestProperties) {
        var matches = [];

        // This flag is needed to match SQL behavior that treats null values as completely unable to
        // be compared, such that (condition-involving-null) == !(condition-involving-null) == false
        this._withinLogicalNot = false;

        // This flag dictates whether we match the aforementioned SQL behavior with null values,
        // or use our own, more sophisticated (and logical) algorithms
        this._strictMode = criteria.strictSQLFiltering;
        if (this._strictMode == null) this._strictMode = this.strictSQLFiltering;


        var recursiveCall = false;
        if (!this._applyingCriteriaToList) {
            this._startApplyingCriteriaToList();
        } else {

            recursiveCall = true;
        }

        for (var idx = 0; idx < data.length; idx++) {

            // The AdvancedCriteria system makes this very easy - just call evaluateCriterion
            // on the top-level criterion, and it handles all the recursion and evaluation of
            // sub-criteria that it needs to do automatically.
            if (!criteria.operator || this.evaluateCriterion(data[idx], criteria)) {
                matches.add(data[idx]);
            }
        }
        if (!recursiveCall) this._endApplyingCriteriaToList();

        return matches;
    },


    _startApplyingCriteriaToList : function () {
        this._applyingCriteriaToList = true;
        this._testedCriteria = [];
        this._shouldDropCriteria = [];
    },
    _endApplyingCriteriaToList : function () {
        delete this._testedCriteria;
        delete this._shouldDropCriteria;
        delete this._applyingCriteriaToList;
    },

    compareAdvancedCriteria : function (newCriterion, oldCriterion, requestProperties) {
        // run through simplifier so we can compare nested and/or's etc correctly.
        // Pass in the parameter to return null if the criterion turns out to be
        // an and/or with no sub criteria so we can identify this case (basically empty criteria).
        newCriterion = isc.DataSource.simplifyAdvancedCriteria(newCriterion, true);
        oldCriterion = isc.DataSource.simplifyAdvancedCriteria(oldCriterion, true);
        if (oldCriterion == null) return newCriterion == null ? 0 : 1;
        if (newCriterion == null) return -1;

        // Normalize the criteria so that the comparison of arrays will work correctly
        newCriterion = this.normalizeAdvancedCriteria(newCriterion);
        oldCriterion = this.normalizeAdvancedCriteria(oldCriterion);

        var op = this.getSearchOperator(oldCriterion.operator);
        var newOp = this.getSearchOperator(newCriterion.operator);

        // If the operation and the new operation are both undefined return zero to indicate they
        // are the same and do not bother with the rest of the logic.
        if(!op && !newOp) {
            return 0;
        }

        if (op != newOp) {
            // Usually 2 different operators essentially means there's no way to compare
            // the 2 criteria and determine which is more restrictive.
            // In this case we return -1 (code for new criteria being less restrictive) indicating
            // that a new server-fetch is required.
            //
            // AND / OR are special cases - they could contain a subcriterion with an operator
            // matching this one - for example:
            //   {operator:"equals", value:"foo"}
            // and
            //   {operator:"or",
            //        criteria:[{operator:"equals", value:"foo"}, {operator:"equals", value:"moo"}]}
            //
            // This leads to a couple of cases where we can perform a meaningful comparison and
            // potentially return +1 [more restrictive] with mismatch operators. Specifically:
            // - old criteria is "OR" and contains subcriterion that matches, or is known
            //   less restrictive than new criteria -- additional subcriteria of the OR make
            //   it less restrictive still, so we know the new criteria is more restrictive and
            //   should return +1
            // - new criteria is "AND" and contains subcriterion that matches or is known more
            //   restrictive than old criterion. In this case additional subcriteria on the new crit
            //   make it more restrictive still, so return +1.


            var rtnVal = -1;
            if (op && op.ID == "or") {
                var extraLayer = {operator:"or", criteria:[newCriterion]};
                rtnVal = op.compareCriteria(extraLayer, oldCriterion, op, this);
            }
            if (rtnVal != -1) return rtnVal;
            if (newOp && newOp.ID == "and") {
                var extraLayer = {operator:"and", criteria:[oldCriterion]};
                return newOp.compareCriteria(newCriterion, extraLayer, newOp, this);
            }


            return -1;
        }

        return op.compareCriteria(newCriterion, oldCriterion, op, this);

    },

    //> @method dataSource.splitCriteria
    // Split a criteria apart based on <code>fields</code>. A new simple criteria
    // is returned with any criteria applicable to the specified fields. The passed
    // <code>criteria</code> is then <u>modified</u> to remove these fields resulting in
    // two distinct criteria.
    // <P>
    // Incoming criteria can be a simple or advanced criteria. For an +link{AdvancedCriteria}
    // only a single level of criteria with a top-level operator of "and" is supported.
    // <P>
    // To avoid modifying an original criteria, use +link{dataSource.copyCriteria} to
    // make a copy to be passed in.
    //
    // @param criteria (Criteria) criteria to be split. May be modified if criteria is
    //                            extracted.
    // @param fields (Array of String) list of fields to extract from criteria
    // @return (Criteria) extracted criteria
    // @visibility external
    //<
    splitCriteria : function (criteria, fields) {
        var splitCriteria = {},
            sourceCriteria = criteria,
            advanced = false
        ;
        if (criteria == null || fields == null) return splitCriteria;

        // Convert advancedCriteria to a simple one
        if (this.isAdvancedCriteria(criteria)) {
            sourceCriteria = this._convertAdvancedCriteriaToSimple(criteria);
            advanced = true;
        }
        if (sourceCriteria == null) return splitCriteria;

        // Simple criteria is just an object with properties that match field names
        // and values that are to be matched.
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (sourceCriteria[field] != null) {
                splitCriteria[field] = sourceCriteria[field];

                // Remove field from original criteria
                if (!advanced) {
                    // Simple criteria
                    delete criteria[field];
                } else {
                    if (criteria.fieldName) {
                        // Advanced criteria, stand-alone criteria
                        delete criteria.fieldName;
                        delete criteria.value;
                    } else {
                        // Advanced criteria, multiple criteria
                        for (var c = 0; c < criteria.criteria.length; c++) {
                            var crit = criteria.criteria[c];
                            if (crit.fieldName == field) {
                                criteria.criteria.removeAt(c);
                                break;
                            }
                        }
                    }
                }
            }
        }

        // Clean up original advancedCriteria if it no longer includes any criteria
        if (advanced && criteria.criteria && criteria.criteria.length == 0) {
            // Effectively return client-side criteria to a empty simple criteria
            delete criteria.criteria;
            delete criteria.operator;
            delete criteria._constructor;
        }
        return splitCriteria;
    },

    // splitCriteria helper
    _convertAdvancedCriteriaToSimple : function (criteria) {
        var simpleCriteria = {};

        // Is this a standalone criteria (i.e. no operator)
        if (criteria.fieldName) {
            if (criteria.operator != "equals") {
                isc.logWarn("splitCriteria: advanced criteria operator, " +
                            criteria.operator + ", not supported.");
                return null;
            }
            simpleCriteria[criteria.fieldName] = criteria.value;
            return simpleCriteria;
        }

        // Only support a high-level operator of "And"
        if (criteria.operator != "and") {
            isc.logWarn("splitCriteria: advanced criteria top-level operator, " +
                        criteria.operator + ", not supported.");
            return null;
        }

        // Check out each criterion. Build simple criteria object.
        for (var i = 0; i < criteria.criteria.length; i++) {
            var crit = criteria.criteria[i];
            if (crit.operator != "equals") {
                isc.logWarn("splitCriteria: advanced criteria second-level operator, " +
                            criteria.operator + ", not supported.");
                return null;
            }
            simpleCriteria[crit.fieldName] = crit.value;
        }

        return (isc.isAn.emptyObject(simpleCriteria) ? null : simpleCriteria);
    }

});


// -------------------------------------------------------------------------------------------------
// DataSource.stringMethods
isc.DataSource.registerStringMethods({
    // register transformResponse and transformRequest as stringMethods - this
    // allows observation of parameters
    transformRequest:"dsRequest",
    transformResponse:"dsResponse,dsRequest,data"

});



// Define temporary function to init the built-in operators - called at the end of the definition
isc._initBuiltInOperators = function () {


    var equality = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {

        if (dataSource._strictMode) {
            if (fieldValue == null || criterionValues.value == null) {
                return (this.negate ? !dataSource._withinLogicalNot : dataSource._withinLogicalNot);
            }
        }

        if (isc.isA.Date(criterionValues.value) && isc.isA.Date(fieldValue)) {
            return (dataSource.compareDates(criterionValues.value, fieldValue, fieldName) == 0);
        } else {
            return (criterionValues.value == fieldValue);
        }
    };

    var rangeCheck = function (fieldName, fieldValue, criterionValues, dataSource, isDateField, nullMeansNull) {
        var start = criterionValues.start || criterionValues.value;
        var end = criterionValues.end || criterionValues.value;

        if (dataSource._strictMode) {
            if (fieldValue == null || (this.lowerBounds && start == null) ||
                (this.upperBounds && end == null)) {
                return  dataSource._withinLogicalNot;
            }
        }



        var lowerBoundsCheck = true;
        var upperBoundsCheck = true;
        var isDate = isc.isA.Date(fieldValue);
        var isNumber = isc.isA.Number(fieldValue);
        var isString = isc.isA.String(fieldValue);

        if (this.lowerBounds && start && ((isNumber && isNaN(start)) ||
                (start && isDate && (!isc.isA.Date(start))) ||
                (start && isString && (!isc.isA.String(start) && !isc.isA.Number(start))))) {
            return false;
        }

        if (this.upperBounds && end &&
            ((isNumber && isNaN(end)) ||
                (end && isDate && (!isc.isA.Date(end))) ||
                (end && isString && (!isc.isA.String(end) && !isc.isA.Number(end))))) {
            return false;
        }

        var undef;
        if (!nullMeansNull) {
            if (start === null || start === undef) {
                lowerBoundsCheck = false;
            }

            if (end === null || end === undef) {
                upperBoundsCheck = false;
            }

            if (isDate && !isc.isA.Date(start)) lowerBoundsCheck = false;
            if (isDate && !isc.isA.Date(end)) upperBoundsCheck = false;
        }

        isDate = isc.isA.Date(start) || isc.isA.Date(end);
        isNumber = isc.isA.Number(start) || isc.isA.Number(end);
        isString = isc.isA.String(start) || isc.isA.String(end);
        var ignoreCase = isString && this.caseInsensitive;

        if (fieldValue === null || fieldValue === undef) {
            if (isDate) {
                fieldValue = new Date(-8640000000000000);
            } else if (isNumber) {
                fieldValue = Number.MIN_VALUE;
            } else {
                fieldValue = "";
            }
        } else {
            if (isNumber && isNaN(fieldValue)) {
                start = "" + start;
                end = "" + end;
            }
            // So, we've been handed a Date object to test against a text data value. Whatever we might
            // try to do here will seem wrong to somebody. So, in the absence of anything more sensible,
            // we'll just return false (so all records are excluded when you have this kind of mismatch)
            if (isDate && !isc.isA.Date(fieldValue)) {
                return false;
            }
        }

        // >, >=, lower bounds check on between, betweenInclusive
        if (this.lowerBounds && lowerBoundsCheck) {
            if (dataSource.compareValues(start, fieldValue, fieldName, ignoreCase) <= (this.inclusive ? -1 : 0)) {
                return false;
            }
        }

        // <, <=, upper bounds check on between, betweenInclusive
        if (this.upperBounds && upperBoundsCheck) {
            if (dataSource.compareValues(end, fieldValue, fieldName, ignoreCase) >= (this.inclusive ? 1 : 0)) {
                return false;
            }
        }

        return true;
    };

    var stringComparison = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        if (isc.isA.Number(fieldValue)) {
            fieldValue = "" + fieldValue;
        }

        if (isc.isA.Boolean(fieldValue)) {
            isc.logWarn("Attempt to compare a Boolean as String, testing value " + criterionValues.value +
                " with operator " + this.ID +
                ". This is an invalid comparison, so we are returning FALSE.");
            return false;
        }

        if (isc.isA.Date(fieldValue)) {
            isc.logWarn("Attempt to compare a Date as String, testing value " + criterionValues.value +
                " with operator " + this.ID +
                ". This is an invalid comparison, so we are returning FALSE.");
            return false;
        }

        // Special-case code to match server-side exception when we get a request to do a string-
        // match on a non-text field (note that numbers are OK - they are converted above)
        if (fieldValue != null && !isc.isA.String(fieldValue)) {
            return true;
        }

        // - a null data value cannot contain anything, including null.
        // - a non-null data value is considered to contain null. We originally followed Javascript
        //   and considered non-null values to NOT contain null; however, it is not possible to implement
        //   this scheme in Hibernate without resorting to hackery.  It was easier to change the
        //   client-side rule
        if (fieldValue == null) {
            if(dataSource._strictMode) {
                var field = dataSource.getField(fieldName);
                if (field && (field.type == "number" || field.type == "integer" ||
                    field.type == "sequence" || field.type == "float")) {
                    if (criterionValues.value == null || isc.isA.Number(criterionValues.value) || isc.isA.Date(criterionValues.value)) {
                        return (this.negate ? !dataSource._withinLogicalNot : dataSource._withinLogicalNot);
                    } else {
                        return false;
                    }
                } else {
                    return (this.negate ? !dataSource._withinLogicalNot : dataSource._withinLogicalNot);
                }
            } else {
                return false;
            }
        }

        var test = criterionValues.value || "";

        if (isc.isA.Number(test)) {
            test = "" + test;
        }

        if (!isc.isA.String(test) || !isc.isA.String(fieldValue)) {
            return false;
        }


        if (this.caseInsensitive) {
            fieldValue = fieldValue.toLowerCase();
            test = test.toLowerCase();
        }

        var result = false;

        if (this.startsWith) {
            result = isc.startsWith(fieldValue, test);
        } else if (this.endsWith) {
            result = isc.endsWith(fieldValue, test);
        } else if (this.equals) {
            result = (fieldValue == test);
        } else {
            result = isc.contains(fieldValue, test);
        }

        return result;
    };

    var nullCheck = function(fieldName, fieldValue, criterionValues, dataSource, isDateField) {

        return (fieldValue == null);
    };

    var regexpCheck = function(fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        // Bail out early if value is undefined.  This is to provide cross-browser consistency,
        // because Moz always returns false for a test of RegExp(undefined), whereas all the
        // others (IE, Safari and Chrome tested) return true.  They all return false if the
        // value is null, so we'll mirror that and return false for undefined as well
        if (!criterionValues.value) {
            return false;
        }

        // Dates make no sense for regexp (numbers work OK)
        if (isc.isA.Date(criterionValues.value) || isc.isA.Date(fieldValue) ||
            isc.DateUtil.isRelativeDate(criterionValues.value))
        {
            return false;
        }

        var regex;

        if (this.caseInsensitive) {
            regex = new RegExp(criterionValues.value, "im");
        } else {
            regex = new RegExp(criterionValues.value, "m");
        }

        return regex.test(fieldValue);
    };

    var matchesPatternCheck = function(fieldName, fieldValue, criterionValues, dataSource, isDateField, isContains) {
        // If for some reason the user has set the patternEscapeChar to an empty or undefined
        // value, make sure we abort here.
        if(!dataSource.patternEscapeChar || dataSource.patternEscapeChar.length > 1) {
            throw "dataSource.patternEscapeChar cannot be null or undefined";
        }

        // Lets prep for the state machine. We need the wildcards in arrays to make it easier
        // and more readable below where we check if the character being looked at is a wildcard.
        var singleWildcards = isc.isA.Array(dataSource.patternSingleWildcard) ? dataSource.patternSingleWildcard : [dataSource.patternSingleWildcard || "?", "%"];
        var multiWildcards = isc.isA.Array(dataSource.patternMultiWildcard) ? dataSource.patternMultiWildcard : [dataSource.patternMultiWildcard || "*"];
        var escapeFound = false;
        var searchExpression = "";
        var value = criterionValues.value;

        // Small state machine to go through the value string, find escape characters as well
        // as wildcard characters and build a regular expression.
        for(var i = 0; i < value.length; i++) {
            // If the character matches the patternEscapeChar on the DataSource, set the engine
            // to ESCAPE state and continue with the next character.
            if(value[i] === dataSource.patternEscapeChar) {
                escapeFound = true;
                continue;
            }

            // If the previous character was an escape character then we need to make sure
            // that this character will treated as a string literal by the regex engine.
            if(escapeFound) {
                // We do this by checking if its a special regex character and if it is we
                // prefix it with the regex escape character.
                searchExpression += value[i].replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");

                // Go back to normal state now that the escape has been dealt with.
                escapeFound = false;
            } else {
                // We know that the previous character was not an escape character to lets
                // carry on.

                // If we've come across a multi wildcard, lets add regex .* and continue.
                if(multiWildcards.contains(value[i])) {
                    searchExpression += ".*";
                    continue;
                }

                // If we've come across a single wildcard, lets add regex . and continue.
                if(singleWildcards.contains(value[i])) {
                    searchExpression += ".";
                    continue;
                }

                searchExpression += value[i];
            }
        }

        if (isContains) {
            criterionValues.value = "^.*" + searchExpression + ".*$";
        } else {
            criterionValues.value = "^" + searchExpression + "$";
        }

        // Now lets pass this over to the regexpCheck method and let it do its thing.
        return regexpCheck.apply(this, [fieldName, fieldValue, criterionValues, dataSource, isDateField]);
    };

    var containsPatternCheck = function(fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        return matchesPatternCheck.apply(this,[fieldName,fieldValue,criterionValues,dataSource, isDateField, true]);
    };

    var contains = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        // We don't support strict SQL mode for this operator - it raises too many hard problems
        // on the server side.
        var value = criterionValues.value || [];

        // Convert a non-null single value into an Array containing that value
        if (!isc.isAn.Array(value)) {
            value = [ value ];
        }

        if(isc.isA.Date(fieldValue)) {
            for (var i = 0; i < value.length; i++) {
                var isDate = isc.isA.Date(value[i]);

                // convert relative dates as necessary
                if (isDateField && !isDate && isc.DateUtil.isRelativeDate(value[i])) {
                    value[i] = isc.DateUtil.getAbsoluteDate(value[i]);
                }

                if (isc.isA.Date(value[i]) &&
                    dataSource.compareDates(value[i], fieldValue, fieldName) == 0)
                {
                    return true;
                }
            }
        } else {
            return value.contains(fieldValue);
        }
    };

    // With the new operators condition api this could probably be merged with the equality method.
    var fieldValueCheck = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        // Include all records if the "other" field name is null
        if (criterionValues.value == null) {
            return true;
        }

        // We do not support strict SQL mode for this operator, because different databases behave differently
        // if the two fields are of different types - we can't match a behavior that varies by database.
        var fieldsMatch = false;

        if(isc.isAn.Array(fieldValue) && isc.isAn.Array(criterionValues.otherValue)) {
                fieldsMatch = fieldValue.equals(criterionValues.otherValue);
        } else {
            fieldsMatch = (criterionValues.otherValue == fieldValue);
        }

        // Dates aren't equal in JS unless they are the same object, so we need a special compare
        if (isc.isA.Date(criterionValues.otherValue) && isc.isA.Date(fieldValue)) {
            fieldsMatch = (dataSource.compareDates(criterionValues.otherValue, fieldValue, fieldName,
                criterionValues.value) == 0);
        }

        return fieldsMatch;
    };

    var fieldStringComparison = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        // Include all records if the "other" field name is null
        if (criterionValues.value == null) {
            return true;
        }

        criterionValues.value = criterionValues.otherValue;

        // Calling another operator condition, we need to use apply here in order to fool the scope.
        // This is because this.negate needs to be the correct one according to this operator.
        return stringComparison.apply(this, [fieldName, fieldValue, criterionValues, dataSource, isDateField]);
    };

    var fieldRangeCheck = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        // Include all records if the "other" field name is null
        if (criterionValues.value == null) {
            return true;
        }

        criterionValues.value = criterionValues.otherValue;

        // "tested" is the value of "otherField".  If it is null or undefined, the rangeCheck()
        // method is going to assume that means "no bounds".  That's OK for a straight
        // range check - if you specify "lessThan" and then provide no value to compare
        // against, it seems more likely that you meant "don't check" rather than
        // "exclude everything".  But in the case of a field comparison, the compare value
        // is different for every row, so it seems much more likely that you would want a
        // null value to be considered "low" - so in a "lessThanField" test, you would expect
        // a row to be excluded if the primary field was non-null and the otherField was null.
        // The final param to the rangeCheck() call switches off this "no bounds" behavior
        return rangeCheck.apply(this, [fieldName, fieldValue, criterionValues, dataSource, isDateField, true]);
    };

    var booleanOperator = function (fieldName, fieldValue, criterionValues, dataSource, isDateField) {
        var criterion = criterionValues.criterion;
        var record = criterionValues.record;
        var returnVal;

        if (!criterion.criteria) {
            // This can legitimately happen if the user proceeds with an "empty" FilterBuilder
            // (ie, one they haven't set any values on).  FilterBuilder strips out all the
            // empty conditions and returns an empty array; something further downstream is
            // throwing that empty array away, so by the time we get here criteria is undef.
            // So we'll just set an undef criteria to the empty array and let it drop through
            criterion.criteria = [];
        }
        if (!isc.isAn.Array(criterion.criteria)) {
            isc.logWarn("AdvancedCriteria: found boolean operator where subcriteria was not " +
                "an array.  Subcriteria was: " + isc.echoFull(criterion.criteria));
            return false;
        }

        // Because we now need to match SQL null-handling behavior, we have to return false even
        // for negated clauses such as not(value equals null). Therefore, we can't just return false
        // if we find a null - we need to return false in the normal case, true where we're nested
        // inside a "not" clause, and then toggle back and forth between true and false as we progress
        // through multiple levels of nested "not" clauses.

        if (this.isNot) {
            dataSource._withinLogicalNot = !dataSource._withinLogicalNot;
        }

        for (var idx = 0; idx < criterion.criteria.length; idx++) {
            var bool = dataSource.evaluateCriterion(record, criterion.criteria[idx]);

            if (this.isAnd && !bool) {
                returnVal = false;
            }

            if (this.isNot && bool) {
                returnVal = false;
            }

            if (this.isOr && bool) {
                returnVal = true;
            }

            if (returnVal != null) {
                break;
            }
        }

        // If we get here and returnVal is undefined, "and" or "not" have succeeded, "or" has failed
        if (returnVal == null) {
            returnVal = !this.isOr;
        }

        // Put "within logical not" status back the way it was
        if (this.isNot) {
            dataSource._withinLogicalNot = !dataSource._withinLogicalNot;
        }

        return returnVal;
    };

    // Operator criterion comparators
    var booleanOperatorComp = function (newCriterion, oldCriterion, operator, ds) {
        if (!oldCriterion.criteria) oldCriterion.criteria = [];
        if (!isc.isAn.Array(oldCriterion.criteria)) {
            isc.logWarn("AdvancedCriteria: boolean compareCriteria found " +
                        "where old subcriteria was not an array");
            return -1;
        }
        if (!newCriterion.criteria) newCriterion.criteria = [];
        if (!isc.isAn.Array(newCriterion.criteria)) {
            isc.logWarn("AdvancedCriteria: boolean compareCriteria found " +
                        "where new subcriteria was not an array");
            return -1;
        }
        var rtn,
            thisLevelRtn = 0,
            oldCount = oldCriterion.criteria.length,
            newCount = newCriterion.criteria.length;

        if (newCount > oldCount && operator.isOr) {
            // We can indicate less restrictive right now without having to check the
            // sub-criteria - there's an extra "or" condition
            return -1;
        }

        var oldWork = isc.clone(oldCriterion.criteria);
        var newWork = isc.clone(newCriterion.criteria);



        for (var i = 0; i < oldCount; i++) {
            var oldSubCriterion = oldWork[i];
            var newSubCriterion = i > newCount ? null : newWork[i];
            if (!newSubCriterion ||
                (newSubCriterion && newSubCriterion.fieldName != oldSubCriterion.fieldName ||
                newSubCriterion.operator  != oldSubCriterion.operator  ||
                newSubCriterion.processed == true)) {
                newSubCriterion = null;
                for (var j = 0; j < newCount; j++) {
                    if (newWork[j].processed) continue;
                    if (newWork[j].fieldName == oldSubCriterion.fieldName &&
                        newWork[j].operator  == oldSubCriterion.operator ) {
                        newSubCriterion = newWork[j];
                        break;
                    }
                }
            }

            if (newSubCriterion && oldSubCriterion) {
                newSubCriterion.processed = true;
                rtn = ds.compareAdvancedCriteria(newSubCriterion, oldSubCriterion);
            } else {
                if (oldSubCriterion && !newSubCriterion) {
                    // Fewer criteria
                    if (operator.isOr) rtn = 1;
                    if (operator.isAnd) rtn = -1;
                    if (operator.isNot) rtn = -1;
                }
            }
            if (operator.isAnd && rtn == -1) return -1;
            if (operator.isOr && rtn == -1) return -1;
            if (operator.isNot && rtn == 1) return -1;
            // If any sub-criterion compare comes back as 1 (-1 for NOT), then we should
            // return 1; if they are all 0 we should return 0.
            if (rtn != 0) thisLevelRtn = 1;
        }

        for (var i = 0; i < newCount; i++ ) {
            if (!newWork[i].processed) {
                // Either there are more new criteria than old, or we've found at least one
                // that could not be matched.
                if (operator.isOr) return -1;
                if (operator.isAnd) return 1;
                if (operator.isNot) return -1;
            }
        }

        // If we get here, we can indicate identical / more restrictive
        return thisLevelRtn;
    };

    var equalityComp = function (newCriterion, oldCriterion, operator, ds) {
        // There's no way an equality check can be more restrictive - either it's identical,
        // or it's less restrictive
        if (newCriterion.fieldName == oldCriterion.fieldName) {
            var eq = ds.compareValues(newCriterion.value, oldCriterion.value,
                            newCriterion.fieldName) == 0;
            if (eq) {
                return 0;
            }
        }

        return -1;
    };

    var rangeCheckComp = function (newCriterion, oldCriterion, operator, ds) {
        // We can return 1 as follows:
        // 1 greaterThan/greaterOrEqual. Return 1 if the new value is larger than the old one.
        // 2 lessThan/lessThanOrEqual.   Return 1 if the new value is smaller than the old one.
        // 3 between/betweenInclusive.   Return 1 if the new start value is larger than the old
        //                               start value but not larger than the old end value,
        //                               OR the new end value is smaller than the old end value
        //                               but not smaller than the old start value.
        var fieldName = newCriterion.fieldName;
        if (newCriterion.fieldName == oldCriterion.fieldName) {
            if (operator.upperBounds && operator.lowerBounds) {
                if (ds.compareValues(newCriterion.start, oldCriterion.start, fieldName) == 0) {
                    if (ds.compareValues(newCriterion.end, oldCriterion.end, fieldName) == 0) {
                        return 0;
                    }
                }
            } else {
                if (ds.compareValues(newCriterion.value, oldCriterion.value, fieldName) == 0) {
                    return 0;
                }
            }

            var newStart = newCriterion.start == null ? newCriterion.value : newCriterion.start,
                oldStart = oldCriterion.start == null ? oldCriterion.value : oldCriterion.start,
                newEnd   = newCriterion.start == null ? newCriterion.value : newCriterion.end,
                oldEnd   = oldCriterion.start == null ? oldCriterion.value : oldCriterion.end;

            var isDate,
                isnumber;

            var oldLowerBounds = true,
                oldUpperBounds = true,
                newLowerBounds = true,
                newUpperBounds = true;

             // See the IDoc in the rangeCheck() function for a discussion of why we're doing this
            if (oldStart == null) oldLowerBounds = false;
            if (oldEnd == null) oldUpperBounds = false;
            if (newStart == null) newLowerBounds = false;
            if (newEnd == null) newUpperBounds = false;

            if (operator.lowerBounds && !operator.upperBounds &&
                    !newLowerBounds && !oldLowerBounds) {
                return 0;
            }

            if (operator.lowerBounds && !operator.upperBounds) {
                if (newLowerBounds && !oldLowerBounds) {
                    return 1;
                }
                if (oldLowerBounds && !newLowerBounds) {
                    return -1;
                }
                var rslt = ds.compareValues(newStart, oldStart, fieldName);
                if (rslt == -1) {
                    return 1;
                } else if (rslt == 0) {
                    return 0;
                }
            }

            if (operator.upperBounds && !operator.lowerBounds &&
                    !newUpperBounds && !oldUpperBounds) {
                return 0;
            }

            if (operator.upperBounds && !operator.lowerBounds) {
                if (newUpperBounds && !oldUpperBounds) {
                    return 1;
                }
                if (oldUpperBounds && !newUpperBounds) {
                    return -1;
                }
                var rslt = ds.compareValues(newStart, oldStart, fieldName);
                if (rslt == 1) {
                    return 1;
                } else if (rslt == 0) {
                    return 0;
                }
            }

            if (operator.lowerBounds && operator.upperBounds) {
                // Technically, checking for >= and <= here is not sufficient to infer more
                // restriction; however, it IS enough to infer either more restriction or an
                // identical pair of filters, and since we won't get this far if the filters
                // are identical, it's safe to return 1.
                if (ds.compareValues(newStart, oldStart, fieldName) <= 0 &&
                    ds.compareValues(newStart, oldEnd, fieldName) >= 0 &&
                    ds.compareValues(newEnd, oldEnd, fieldName) >= 0 &&
                    ds.compareValues(newEnd, oldStart, fieldName) <= 0)
                {
                    return 1;
                }
                if ((newLowerBounds && !oldLowerBounds) ||
                    (newUpperBounds && !oldUpperBounds)) {
                    return 1;
                }
                if (!newLowerBounds && !oldLowerBounds &&
                    !newUpperBounds && !oldLowerBounds) {
                    return 0;
                }
            }
        }
        return -1;
    };

    var stringComparisonComp = function (newCriterion, oldCriterion, operator) {
        // We can return 1 as  follows:
        // 1 startsWith. Return 1 if the new value startsWith the old value AND is longer.
        //               Obey the caseInsensitive flag.
        //               If negate == true, the test is reversed - ie, old value must
        //               startWith new value, and new value must be shorter.
        // 2 endsWith.   Return 1 if the new value endsWith the old value AND is longer.
        //               Obey the caseInsensitive flag.
        //               If negate == true, the test is reversed - ie, old value must
        //               endWith new value, and new value must be shorter.
        // 3 contains.   Return 1 if the old value is a proper substring of the new value (ie,
        //               new value both contains and is longer than old value). Obey the
        //               caseInsensitive flag. If negate == true, the new field must be a
        //               substring of the old one.
        // 4 equals.     Return 0 if the new value exactly matches the old one.  This
        //               comparison is actually only used when in case-insensitive mode;
        //               obviously, we simply compare for equality otherwise.

        var oldVal = oldCriterion.value;
        var newVal = newCriterion.value;

        // Convert numbers to strings - other than that, reject anything that isn't a string
        if (isc.isA.Number(oldVal)) oldVal = "" + oldVal;
        if (isc.isA.Number(newVal)) newVal = "" + newVal;

        if (!isc.isA.String(oldVal) || !isc.isA.String(newVal)) return -1;

        if (operator.caseInsensitive) {
            oldVal = oldVal.toLowerCase();
            newVal = newVal.toLowerCase();
        }

        if (newCriterion.fieldName == oldCriterion.fieldName
                && newCriterion.value == oldCriterion.value
                && !operator.equals)
        {
            return 0;
        }

        if (operator.equals) {
            return oldVal == newVal ? 0 : -1;
        }

        if (operator.startsWith && !operator.negate &&
                newVal.length > oldVal.length && isc.startsWith(newVal, oldVal))
        {
            return 1;
        }


        if (operator.startsWith && operator.negate &&
                oldVal.length > newVal.length && isc.startsWith(oldVal, newVal))
        {
            return 1;
        }

        if (operator.endsWith && !operator.negate &&
                newVal.length > oldVal.length && isc.endsWith(newVal, oldVal))
        {
            return 1;
        }

        if (operator.endsWith && operator.negate &&
                oldVal.length > newVal.length && isc.endsWith(oldVal, newVal))
        {
            return 1;
        }

        if (!operator.startsWith && !operator.endsWith && !operator.negate &&
                newVal.length > oldVal.length && isc.contains(newVal, oldVal))
        {
            return 1;
        }

        if (!operator.startsWith && !operator.endsWith && operator.negate &&
                oldVal.length > newVal.length && isc.contains(oldVal, newVal))
        {
            return 1;
        }

        return -1;
    };

    var nullCheckComp = function (newCriterion, oldCriterion, operator) {
        if (newCriterion.fieldName == oldCriterion.fieldName)  {
            return 0;
        }

        return -1;
    };

    var regexpCheckComp = function (newCriterion, oldCriterion, operator) {
        // We can't know whether the new regexp is more or less restrictive - either they
        // match, or we indicate less restrictive
        if (newCriterion.value == oldCriterion.value &&
                  newCriterion.fieldName == oldCriterion.fieldName) {
            return 0;
        }

        return -1;
    };

    var patternCheckComp = function (newCriterion, oldCriterion, operator) {
        if (newCriterion.value == oldCriterion.value &&
                  newCriterion.fieldName == oldCriterion.fieldName) {
            return 0;
        }
        // The only case when we can determine that pattern became less restrictive is when
        // user added non wildcard symbols to it.
        if (newCriterion.value.length > oldCriterion.value.length &&
                newCriterion.fieldName == oldCriterion.fieldName) {
            var newIndex = 0;
            var oldIndex = 0;
            var wildcardChar = "*";
            var found = false;
            while (oldIndex < oldCriterion.value.length) {
                while (newIndex < newCriterion.value.length) {
                    if (newCriterion.value[newIndex] == oldCriterion.value[oldIndex]) {
                        found = true
                        newIndex++;
                        break;
                    }
                    if (newCriterion.value[newIndex] == wildcardChar) {
                        return -1;
                    }
                    newIndex++;
                }
                oldIndex++;
            }
            // pattern end part
            while (newIndex < newCriterion.value.length) {
                if (newCriterion.value[newIndex] == wildcardChar) {
                    return -1;
                }
                newIndex++;
            }

            return found ? 1 : -1;
        }
        return -1;
    };

    var setMembershipComp = function (newCriterion, oldCriterion, operator) {
        // We can return 1 if the new set is a proper subset of the old one
        // (if negate == true, the old set must be a proper subset of the new one)
        if (newCriterion.fieldName == oldCriterion.fieldName) {

            // We can only sensibly compare Arrays
            if (!isc.isAn.Array(oldCriterion.value) || !isc.isAn.Array(newCriterion.value)) {
                return -1;
            }

            if (newCriterion.value.equals(oldCriterion.value)) {
                return 0;
            }

            if (!operator.negate && oldCriterion.value.containsAll(newCriterion.value)) {
                return 1;
            }

            if (operator.negate && newCriterion.value.containsAll(oldCriterion.value)) {
                return 1;
            }
        }

        return -1;
    };

    var fieldValueCheckComp = function (newCriterion, oldCriterion, operator) {
        // There's no way this check can be more restrictive - either it's the same
        // or it's different, and if it's different we have to go back to the server.
        if (newCriterion.value == oldCriterion.value &&
                  newCriterion.fieldName == oldCriterion.fieldName) {
            return 0;
        }

        return -1;
    };

    var builtinOps = [
    {
        ID: "equals",
        titleProperty: "equalsTitle",
        negate: false,
        valueType: "fieldType",
        condition: equality,
        compareCriteria: equalityComp,
        symbol: "==",
        wildCard: "*",
        canNormalize: true,
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },

    {
        ID: "notEqual",
        titleProperty: "notEqualTitle",
        negate: true,
        valueType: "fieldType",
        condition: equality,
        compareCriteria: equalityComp,
        symbol: "!",
        wildCard: "*",
        canNormalize: true,
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "greaterThan",
        titleProperty: "greaterThanTitle",
        lowerBounds: true,
        valueType: "fieldType",
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: ">",
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "lessThan",
        titleProperty: "lessThanTitle",
        upperBounds: true,
        valueType: "fieldType",
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: "<",
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "greaterOrEqual",
        titleProperty: "greaterOrEqualTitle",
        lowerBounds: true,
        inclusive: true,
        valueType: "fieldType",
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: ">=",
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "lessOrEqual",
        titleProperty: "lessOrEqualTitle",
        upperBounds: true,
        inclusive: true,
        valueType: "fieldType",
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: "<=",
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };

            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "between",
        titleProperty: "betweenTitle",
        lowerBounds: true,
        upperBounds: true,
        hidden:true,
        valueType: "valueRange",
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };
            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "iBetween",
        titleProperty: "iBetweenTitle",
        lowerBounds: true,
        upperBounds: true,
        hidden:true,
        valueType: "valueRange",
        condition: rangeCheck,
        caseInsensitive: true,
        compareCriteria: rangeCheckComp,
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };
            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "betweenInclusive",
        titleProperty: "betweenInclusiveTitle",
        lowerBounds: true,
        upperBounds: true,
        valueType: "valueRange",
        inclusive: true,
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: "...",
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };
            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "iBetweenInclusive",
        titleProperty: "iBetweenInclusiveTitle",
        lowerBounds: true,
        upperBounds: true,
        valueType: "valueRange",
        inclusive: true,
        condition: rangeCheck,
        compareCriteria: rangeCheckComp,
        symbol: "...",
        caseInsensitive: true,
        getCriterion : function (fieldName, item) {
            var result = { fieldName: fieldName, operator: this.ID };
            if (isc.isA.RelativeDateItem(item))
                result.value = item.getRelativeDate() || item.getValue();
            else result.value = item.getValue();

            return result;
        }
    },
    {
        ID: "iEquals",
        titleProperty: "iEqualsTitle",
        equals: true,
        caseInsensitive: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "==",
        wildCard: "*",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iContains",
        titleProperty: "iContainsTitle",
        caseInsensitive: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "~",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iStartsWith",
        titleProperty: "iStartsWithTitle",
        startsWith: true,
        caseInsensitive: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "^",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iEndsWith",
        titleProperty: "iEndsWithTitle",
        endsWith: true,
        caseInsensitive: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "|",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "contains",
        titleProperty: "containsTitle",
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "~",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "startsWith",
        titleProperty: "startsWithTitle",
        startsWith: true,
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "^",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "endsWith",
        titleProperty: "endsWithTitle",
        endsWith: true,
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "|",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iNotEqual",
        titleProperty: "iNotEqualTitle",
        caseInsensitive: true,
        equals: true,
        negate: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!",
        wildCard: "*",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iNotContains",
        titleProperty: "iNotContainsTitle",
        caseInsensitive: true,
        negate: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!~",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iNotStartsWith",
        titleProperty: "iNotStartsWithTitle",
        startsWith: true,
        caseInsensitive: true,
        negate: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!^",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "iNotEndsWith",
        titleProperty: "iNotEndsWithTitle",
        endsWith: true,
        caseInsensitive: true,
        negate: true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!@",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "notContains",
        titleProperty: "notContainsTitle",
        negate: true,
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!~",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "notStartsWith",
        titleProperty: "notStartsWithTitle",
        startsWith: true,
        negate: true,
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!^",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "notEndsWith",
        titleProperty: "notEndsWithTitle",
        endsWith: true,
        negate: true,
        hidden:true,
        valueType: "fieldType",
        condition: stringComparison,
        symbol: "!@",
        canNormalize: true,
        compareCriteria: stringComparisonComp
    },
    {
        ID: "isNull",
        titleProperty: "isNullTitle",
        valueType: "none",
        condition: nullCheck,
        symbol: "#",
        compareCriteria: nullCheckComp
    },
    {
        ID: "notNull",
        titleProperty: "notNullTitle",
        negate: true,
        valueType: "none",
        condition: nullCheck,
        symbol: "!#",
        compareCriteria: nullCheckComp
    },
    {
        ID: "regexp",
        titleProperty: "regexpTitle",
        hidden: true,
        valueType: "custom",
        condition: regexpCheck,
        symbol: "/regex/",
        compareCriteria: regexpCheckComp
    },
    {
        ID: "iregexp",
        titleProperty: "iregexpTitle",
        hidden: true,
        caseInsensitive: true,
        valueType: "custom",
        condition: regexpCheck,
        symbol: "/regex/",
        compareCriteria: regexpCheckComp
    },
    {
        ID: "matchesPattern",
        titleProperty: "matchesPatternTitle",
        hidden: true,
        valueType: "custom",
        symbol: "==i~",
        wildcard: "*",
        condition: matchesPatternCheck,
        compareCriteria: patternCheckComp
    },
    {
        ID: "iMatchesPattern",
        titleProperty: "iMatchesPatternTitle",
        hidden: true,
        caseInsensitive: true,
        valueType: "custom",
        symbol: "==~",
        wildcard: "*",
        condition: matchesPatternCheck,
        compareCriteria: patternCheckComp
    },
    {
        ID: "containsPattern",
        titleProperty: "containsPatternTitle",
        hidden: true,
        valueType: "custom",
        symbol: "=i~",
        wildcard: "*",
        condition: containsPatternCheck,
        compareCriteria: patternCheckComp
    },
    {
        ID: "iContainsPattern",
        titleProperty: "iContainsPatternTitle",
        hidden: true,
        caseInsensitive: true,
        valueType: "custom",
        symbol: "=~",
        wildcard: "*",
        condition: containsPatternCheck,
        compareCriteria: patternCheckComp
    },
    {
        ID: "inSet",
        titleProperty: "inSetTitle",
        valueType: "valueSet",
        condition: contains,
        compareCriteria: setMembershipComp,
        symbol: "=(",
        closingSymbol: ")",
        valueSeparator: "|",
        processValue : function (value, ds) {
            return value.split(this.valueSeparator);
        }
    },
    {
        ID: "notInSet",
        titleProperty: "notInSetTitle",
        negate: true,
        valueType: "valueSet",
        condition: contains,
        compareCriteria: setMembershipComp,
        symbol: "!=(",
        closingSymbol: ")",
        valueSeparator: "|",
        processValue : function (value, ds) {
            return value.split(this.valueSeparator);
        }
    },
    {
        ID: "equalsField",
        titleProperty: "equalsFieldTitle",
        valueType: "fieldName",
        condition: fieldValueCheck,
        symbol: "=.",
        canNormalize: true,
        compareCriteria: fieldValueCheckComp,
        processValue : function (value, ds) {
            if (!ds) return value;

            var field = ds.getField(value);

            if (field) return value;

            field = ds.getFieldByTitle(value);

            if (field) return field.name;

            return null;
        }
    },
    {
        ID: "notEqualField",
        titleProperty: "notEqualFieldTitle",
        negate: true,
        valueType: "fieldName",
        condition: fieldValueCheck,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iEqualsField",
        titleProperty: "iEqualsFieldTitle",
        equals: true,
        caseInsensitive:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iNotEqualField",
        titleProperty: "iNotEqualFieldTitle",
        equals: true,
        caseInsensitive:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "greaterThanField",
        titleProperty: "greaterThanFieldTitle",
        lowerBounds: true,
        valueType: "fieldName",
        condition: fieldRangeCheck,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "lessThanField",
        titleProperty: "lessThanFieldTitle",
        upperBounds: true,
        valueType: "fieldName",
        condition: fieldRangeCheck,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "greaterOrEqualField",
        titleProperty: "greaterOrEqualFieldTitle",
        lowerBounds: true,
        inclusive: true,
        valueType: "fieldName",
        condition: fieldRangeCheck,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "lessOrEqualField",
        titleProperty: "lessOrEqualFieldTitle",
        upperBounds: true,
        inclusive: true,
        valueType: "fieldName",
        condition: fieldRangeCheck,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "containsField",
        titleProperty: "containsFieldTitle",
        hidden:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "startsWithField",
        titleProperty: "startsWithTitleField",
        startsWith: true,
        hidden:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "endsWithField",
        titleProperty: "endsWithTitleField",
        endsWith: true,
        hidden:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iContainsField",
        titleProperty: "iContainsFieldTitle",
        hidden:true,
        caseInsensitive:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iStartsWithField",
        titleProperty: "iStartsWithTitleField",
        startsWith: true,
        hidden:true,
        caseInsensitive:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iEndsWithField",
        titleProperty: "iEndsWithTitleField",
        endsWith: true,
        hidden:true,
        caseInsensitive:true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "notContainsField",
        titleProperty: "notContainsFieldTitle",
        hidden:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "notStartsWithField",
        titleProperty: "notStartsWithTitleField",
        startsWith: true,
        hidden:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "notEndsWithField",
        titleProperty: "notEndsWithTitleField",
        endsWith: true,
        hidden:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iNotContainsField",
        titleProperty: "iNotContainsFieldTitle",
        hidden:true,
        caseInsensitive:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iNotStartsWithField",
        titleProperty: "iNotStartsWithTitleField",
        startsWith: true,
        hidden:true,
        caseInsensitive:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "iNotEndsWithField",
        titleProperty: "iNotEndsWithTitleField",
        endsWith: true,
        hidden:true,
        caseInsensitive:true,
        negate: true,
        valueType: "fieldName",
        condition: fieldStringComparison,
        canNormalize: true,
        compareCriteria: fieldValueCheckComp
    },
    {
        ID: "and",
        titleProperty: "andTitle",
        isAnd: true,
        valueType: "criteria",
        condition: booleanOperator,
        symbol: " and ",
        compareCriteria: booleanOperatorComp
    },
    {
        ID: "not",
        titleProperty: "notTitle",
        isNot: true,
        valueType: "criteria",
        condition: booleanOperator,
        compareCriteria: booleanOperatorComp
    },
    {
        ID: "or",
        titleProperty: "orTitle",
        isOr: true,
        valueType: "criteria",
        condition: booleanOperator,
        symbol: " or ",
        compareCriteria: booleanOperatorComp
    }
    ];

    for (var idx = 0; idx < builtinOps.length; idx++) {
        isc.DataSource.addSearchOperator(builtinOps[idx]);
    }

    // Create default typeOperators
    isc.DataSource.setTypeOperators(null, ["equals", "notEqual", "lessThan", "greaterThan",
                                           "lessOrEqual", "greaterOrEqual", "between",
                                           "betweenInclusive", "isNull", "notNull",
                                           "equalsField", "notEqualField",
                                           "greaterThanField", "lessThanField",
                                           "greaterOrEqualField", "lessOrEqualField",
                                           "and", "or", "not"]);

    // these are in addition to the base operators
    isc.DataSource.setTypeOperators("text", ["regexp", "iregexp", "contains", "startsWith",
                                               "endsWith", "iEquals", "iNotEqual",
                                               "iBetween", "iBetweenInclusive",
                                               "iContains", "iStartsWith", "iEndsWith",
                                               "notContains", "notStartsWith", "notEndsWith",
                                               "iNotContains", "iNotStartsWith", "iNotEndsWith",
                                               "containsField", "startsWithField", "endsWithField",
                                               "iEqualsField", "iNotEqualField",
                                               "iContainsField", "iStartsWithField", "iEndsWithField",
                                               "notContainsField", "notStartsWithField", "notEndsWithField",
                                               "iNotContainsField", "iNotStartsWithField", "iNotEndsWithField",
                                               "matchesPattern", "iMatchesPattern", "containsPattern", "iContainsPattern",
                                               "inSet", "notInSet"]);

    isc.DataSource.setTypeOperators("integer", ["iContains", "iStartsWith", "iEndsWith",
                                               "iNotContains", "iNotStartsWith", "iNotEndsWith",
                                               "iEqualsField", "iNotEqualField",
                                               "containsField", "startsWithField", "endsWithField",
                                               "iContainsField", "iStartsWithField", "iEndsWithField",
                                               "notContainsField", "notStartsWithField", "notEndsWithField",
                                               "iNotContainsField", "iNotStartsWithField", "iNotEndsWithField",
                                               "inSet", "notInSet"]);

    isc.DataSource.setTypeOperators("float", ["iContains", "iStartsWith", "iEndsWith",
                                               "iNotContains", "iNotStartsWith", "iNotEndsWith",
                                               "iEqualsField", "iNotEqualField",
                                               "containsField", "startsWithField", "endsWithField",
                                               "iContainsField", "iStartsWithField", "iEndsWithField",
                                               "notContainsField", "notStartsWithField", "notEndsWithField",
                                               "iNotContainsField", "iNotStartsWithField", "iNotEndsWithField",
                                               "inSet", "notInSet"]);

};

isc._initBuiltInOperators();


// Built-in Schema
// ---------------------------------------------------------------------------------------

// A generic "Object" DataSource is needed so that you can declare that a field is of Object
// type, as opposed to simple type, without having to be more specific.
isc.DataSource.create({ ID:"Object", fields: {}, addGlobalId:false });

isc.DataSource.create({
    ID:"ValueMap",
    addGlobalId:false,
    // prevent clobbering by server definitions
    builtinSchema : true,
    // a valueMap can appear as a simple JS Array, and in this case shouldn't be considered
    // multiple valueMaps
    canBeArrayValued : true,
    //acceptsSimpleTypes : true,
    fields : {},
    _$ID : "ID", _$id : "id",
    xmlToJS : function (element, context) {
        if (element == null || isc.xml.elementIsNil(element)) return null;

        var children = isc.xml.getElementChildren(element),
            valueMap = isc.xml.getAttributes(element),
            // any attribute is considered to be an id->value mapping
            gotIDMapping = !isc.isAn.emptyObject(valueMap);
        for (var i = 0; i < children.length; i++) {
            var child = children[i],
                ID = child.getAttribute(this._$ID) || child.getAttribute(this._$id),
                value = isc.xml.getElementText(child);
            if (ID != null && value != null) {
                // <value ID="idValue">displayValue</value>
                gotIDMapping = true;
                valueMap[ID] = value;
            } else if (ID != null) {
                // <value ID="idValue">displayValue</value>
                valueMap[ID] = ID;
            } else if (value != null) {
                // <value>onlyValue</value>
                valueMap[value] = value;
            } else {
                // <value/> or <value></value> means the empty value is legal
                valueMap[isc.emptyString] = isc.emptyString;
            }
        }
        // if we got at least one id -> value mapping, return the valueMap as a map
        if (gotIDMapping) return valueMap;
        // otherwise return it as an array
        return isc.getValues(valueMap);
    },
    xmlSerializeFields : function (data, flags, indent) {
        if (data == null || isc.DS.isSimpleTypeValue(data)) {
            return this.Super("xmlSerializeFields", arguments);
        }

        var output = isc.SB.create(),
            indent = (indent || "") + (!flags || flags.indent != false ? "    " : "");
        if (isc.isAn.Array(data)) {
            for (var i = 0; i < data.length; i++) {
                var value = data[i];
                output.append("\r", indent, "<value>", isc.makeXMLSafe(value), "</value>");
            }
        } else {
            // for objects, output the special valueMap format
            for (var id in data) {
                var value = data[id];
                output.append("\r", indent, "<value id=\"", isc.makeXMLSafe(id), "\">",
                              isc.makeXMLSafe(value), "</value>");
            }
        }
        return output.toString();
    }
});

// --------------------------------------------------------------------------------------------


isc.ClassFactory.defineInterface("DataModel");


isc.DataModel.addInterfaceMethods({

getDataSource : function () {
    if (isc.isA.String(this.dataSource)) this.dataSource = isc.DS.get(this.dataSource);
    return this.dataSource;
},


getOperationId : function (operationType) {
    var operation = this.getOperation(operationType);
    return operation == null ? null : (isc.isA.String(operation) ? operation : operation.ID);
},


getOperation : function (operationType) {
    var application = isc.rpc.getDefaultApplication(),
        operation,
        operationId;

    // see if an operation for this operation type was explicitly provided
    var operationProperty = operationType + "Operation";
    if (this[operationProperty]) {
        operation = this[operationProperty];
        // support operation spec in object or string form - if in object form, just return it
        if (isc.isAn.Object(operation)) return operation;

        // it's a string - use that as the operationId
        operationId = operation;
    }

    // no operation specified, create an auto-operation using the dataSource provided
    if (operationId == null || isc.isA.String(operationId)) {
        var dataSource = this.getDataSource();
        if (dataSource == null) {
            this.logWarn("can't getOperation for type: " + operationType +
                         ", no " + operationProperty + " specified, and no dataSource to " +
                         "create an auto-operation");
            return null;
        }
        this.logInfo("creating auto-operation for operationType: " + operationType);
        operation = isc.DataSource.makeDefaultOperation(dataSource, operationType, operationId);

        // cache the auto-operation
        operationId = operation.ID;
        this[operationProperty] = operationId;
    }

    return operation;
}

});

//> @class XJSONDataSource
// A DataSource preconfigured to use the +link{type:RPCTransport,"scriptInclude"} transport
// (sometimes called "JSONP") for cross-domain calls to JSON services.
// <P>
// To use this DataSource, provide the URL of the service as +link{DataSource.dataURL}, and
// provide +link{DataSource.fields,fields} that describe the structure of the data you want to
// extract from the service's response.
// <P>
// +link{DataSource.recordXPath} and +link{DataSourceField.valueXPath} can be used to extract
// data from the JSON structure returned by the service.  See
// +link{group:clientDataIntegration,Client-Side Data Integration} for an overview of how to
// control what parts of the JSON structure are included in the +link{DSResponse} object, and
// hence provided to +link{DataBoundComponent}s that are bound to this DataSource.
// <P>
// This XJSONDataSource is really a subclass of DataSource with just a few property settings:
// <pre>
//    dataFormat : "json",
//    dataTransport : "scriptInclude"
//    callbackParam : "callback"
// </pre>
// <P>
// If you are also writing the server side code to respond to requests from this DataSource,
// see the
// +externalLink{http://developer.yahoo.net/common/json.html#callbackparam,tutorial provided by Yahoo!}
// for a good overview of how this transport mechanism works.  Note, as indicated in the
// tutorial above, the server is responsible for writing out not just the data, but also a
// JavaScript function call that tells the client that the response has arrived.  The client
// passes the name of the function to call as the "callback" URL parameter.
// <P>
// NOTE: if you use this DataSource to contact Yahoo web services, remember to include
// output=json in the dataURL, as well as a
// +externalLink{http://developer.yahoo.net/,Yahoo developer ID}.
//
// @treeLocation Client Reference/Data Binding
// @visibility xmlBinding
// @example jsonYahooWebServices
//<
isc.defineClass("XJSONDataSource", "DataSource").addMethods({
    dataFormat : "json",
    dataTransport : "scriptInclude"
});


//> @groupDef clientDataIntegration
// SmartClient supports declarative, XPath-based binding of visual components to any server
// capable of returning XML or JSON responses over HTTP, without the need for the
// +link{group:serverDataIntegration,SmartClient server}.
// <P>
// This approach is called Client-Side Data Integration, which means:
// <ul>
// <li> You +link{group:dataSourceDeclaration,create DataSources}
// <smartclient>in JavaScript</smartclient>
// <smartgwt>programmatically in Java (with <code>new DataSource()</code>)</smartgwt>
// which describe the data to be loaded and manipulated in the user interface. The
// JavaScript that creates these DataSources may be dynamically generated and/or existing
// metadata may be +link{group:metadataImport,imported}.
// <li> You configure DataSources, via property and method overrides, to send appropriate
// HTTP requests to your server, and to parse HTTP responses from your server, in order to
// fulfill the 4 core operations of the +link{group:dataSourceOperations,DataSource Protocol}.
// <li> These DataSources are then bound to
// +link{dataBoundComponent,databinding-capable UI components}, which can provide a variety of
// complete user interactions (form-based editing, grid-based editing, load on demand, ..)
// based on these 4 core operations
// </ul>
// <P>
// <h4>Approaches and platforms</h4>
// <P>
// <B>REST integration with RestDataSource (preferred)</B>
// <P>
// The +link{RestDataSource} provides a complete XML or JSON-based protocol that supports all
// of the features of SmartClient's databinding layer (data paging, queuing/batching of
// requests for transactions, nested AdvancedCriteria, server-side validation errors, automatic
// cache synchronization, etc).  To use the RestDataSource, simply write server code that can
// parse RestDataSource requests and produce the required responses; example requests and
// responses are +link{RestDataSource,provided}.
// <P>
// The SmartClient public wiki contains examples of integration with
// +externalLink{http://wiki.smartclient.com/display/Main/Integrating+with+ASP.Net+MVC,.NET's ASP.NET MVC}
// as well as
// +externalLink{http://wiki.smartclient.com/pages/viewpage.action?pageId=1442411,PHP with Doctrine}.
// <P>
// <h4>Consuming Existing XML and JSON formats</h4>
// <p>
// If you have pre-existing XML or JSON formats, SmartClient DataSources can be configured to
// work with them.  However, <b>only use this approach if you are unable to modify a
// pre-existing protocol</b>.  If you have a choice, use RestDataSource.
// <P>
// In particular, if you are choosing between tools that can automatically generate a REST
// service from an API vs using the pre-built RestDataSource protocol, <b>definitely use
// RestDataSource</b>.  Automatically generated REST interfaces will not handle the needs of a
// modern GUI, such as transactional/batched saves and user-ready validation error messages.
// For a deeper discussion, see
// +externalLink{http://forums.smartclient.com/showthread.php?t=8159#aExistingRest,this FAQ}.
// <P>
// Specifically for pre-existing WSDL web services, see the discussion of WSDL Integration
// below instead.
// <P>
// To display XML or JSON from a pre-existing service in a visual component such as a ListGrid,
// you bind the component to a +link{DataSource} which provides the
// +link{DataSource.dataURL,URL} of the service, as well as a declaration of how to form inputs
// to the service and how to interpret service responses as DataSource records.
// <P>
// An XPath expression, the +link{attr:operationBinding.recordXPath,recordXPath}, is applied to
// the service response to select the XML elements or JSON objects that should be interpreted
// as DataSource records.  Then, for each field of the DataSource, an optional
// +link{attr:DataSourceField.valueXPath} can be declared which selects the value for the field
// from within each of the XML elements or JSON objects selected by the recordXPath.  If no
// valueXPath is specified, the field name itself is taken as an XPath, which will select the
// same-named subelement or property from the record element or object.
// <P>
// For example, the following code defines a DataSource that a ListGrid could bind to in order
// to display an RSS 2.0 feed.
// <PRE>
//    isc.DataSource.create({
//        dataURL:feedURL,
//        recordXPath:"//item",
//        fields:[
//            { name:"title" },
//            { name:"link" },
//            { name:"description" }
//        ]
//    });
// </PRE>
// A representative slice of an RSS 2.0 feed follows:
// <pre>
//     &lt;?xml version="1.0" encoding="iso-8859-1" ?&gt;
//     &lt;rss version="2.0"&gt;
//     &lt;channel&gt;
//       &lt;title&gt;feed title&lt;/title&gt;
//       ...
//       &lt;item&gt;
//         &lt;title&gt;article title&lt;/title&gt;
//         &lt;link&gt;url of article&lt;/link&gt;
//         &lt;description&gt;
//            article description
//         &lt;/description&gt;
//       &lt;/item&gt;
//       &lt;item&gt;
//          ...
// </pre>
// Here, the recordXPath selects a list of &lt;item&gt; elements.  Since the intended values
// for each DataSource field appear as simple subelements of each &lt;item&gt; element (eg
// &lt;description&gt;), the field name is sufficient to select the correct values, and no
// explicit valueXPath needs to be specified.
// <P>
// A running version of this example is available here: +explorerExample{rssFeed}.  Further
// examples of simple XML or JSON data loading using files stored on disk as the
// "service" to contact: the +explorerExample{simpleJSON,Simple JSON} example shows loading
// data from a JSON file into a databound grid, and the
// +explorerExample{xpathBinding,XPath Binding example} shows loading XML and processing it with
// XPaths.
// <P>
// <B>WSDL integration</B>
// <P>
// If you have a choice between WSDL and REST integration, we recommend REST integration - it's
// simpler, requires less specialized knowledge, is easier to troubleshoot and is faster.  If
// you need to use WSDL, see the +link{group:wsdlBinding,WSDL Binding Overview}.
// <P>
// <H4>Round Tripping: Loading, Editing and Saving</H4>
// <P>
// For WSDL web services, see the +link{group:wsdlBinding,WSDL binding topic} first.
// <P>
// When using RestDataSource, see the +link{RestDataSource} docs for message formats for
// saving, as well as expected responses.
// <P>
// When a user triggers a DSRequest (eg, completes an inline edit in a grid), the request
// data will be sent to the dataURL.  The +link{group:dataSourceOperations,DataSource protocol}
// describes request and response data expected for each operation type.
// <P>
// By using settings such as +link{operationBinding.dataProtocol}, you can control how
// DSRequests are sent to your backend so that you can handle them most easily.  By using the
// same properties used to initially load data (eg
// +link{operationBinding.recordXPath,recordXPath}), you can control how SmartClient forms the
// DSResponses that are then interpreted by +link{DataBoundComponent,databound components}.
// <P>
// <b>Controlling how DSRequests are sent</b>
// <P>
// According to the +link{attr:operationBinding.dataProtocol,protocol} being used, the
// +link{group:dataSourceOperations,DataSource request data}, if any, either becomes HTTP
// params (sent by GET or POST), or an XML message as put together by
// +link{method:DataSource.xmlSerialize()}.  For a DataSource invoking a WSDL-described web
// service, XML serialization automatically handles namespacing and SOAP encoding.
// <P>
// Note that, by default, just +link{attr:dsRequest.data} is sent, not any of the metadata such
// as +link{attr:dsRequest.startRow}.  This can be customized via
// +link{method:DataSource.transformRequest()}.
// <P>
// The URL to contact is set via the +link{attr:operationBinding.dataURL,dataURL}
// property.  If using a Web Service, the <code>dataURL</code> defaults to the service location
// URL embedded in the WSDL file.
// <P>
// For example, in the default configuration for non-WSDL binding, since
// +link{operationBinding.dataProtocol,dataProtocol} is "getParams", +link{dsRequest.data} is
// sent as HTTP params in an HTTP "GET" operation.  Given:
// <ul>
// <li> changes to an existing record, hence an "update" request
// <li> a +link{dataSourceField.primaryKey,primaryKey} field of "id" with value "5" on the
// record to be updated
// <li> a field "age" being changed to "32"
// <li> "dataURL" of "save.php"
// </ul>
// You will see an HTTP GET to the URL <code>save.php?id=5&age=32</code>.
// <P>
// <b>Forming a DSResponse from the response data</b>
// <P>
// A +link{class:DSResponse} is created from the response data by using XPath
// expressions declared in the schema (+link{attr:operationBinding.recordXPath,recordXPath} and
// +link{attr:DataSourceField.valueXPath,valueXPath}) to extract DataSource record and field
// values.
// <P>
// See the +explorerExample{xmlEditSave,"Edit and Save"} example for sample XML responses for
// all four operationTypes.
// <P>
// Similar to input processing, by default DataSource layer metadata, such as
// +link{attr:dsResponse.startRow}, is not extracted from the response data.  You can
// implement +link{method:DataSource.transformResponse()} to fill out the metadata fields of
// the +link{class:DSResponse}, in order to allow more DataSource features, such as paging and
// validation errors, to be used with a web service that supports such features.
// <P>
// See the +explorerExample{xmlServerValidationErrors, XML} and
// +explorerExample{jsonServerValidationErrors, JSON} versions of the transformResponse()
// example for an example of providing validation errors in XML or JSON responses.
//
// @visibility xmlBinding
// @treeLocation Concepts/Client-Server Integration
// @treeLocation Client Reference/Data Binding
// @title Client-side Data Integration
//<

//> @groupDef serverScript
// SmartClient allows you to embed "scriptlets" directly in your .ds.xml file to take care of
// simple business logic without having to create a separate file or class to hold the logic.
// <P>
// These scriptlets can be written in any language supported by the Java "JSR 223" standard,
// including Java itself, as well as languages such as Groovy, JavaScript, Velocity, Python, Ruby,
// Scala and Clojure.
// <P>
// Scriptlets are automatically recompiled when you change the .ds.xml file - just reload the page
// and the SmartClient Server Framework automatically notices the modified DataSource file and
// uses the new scriptlets.
// <P>
// <b>Declaring Scriptlet Language</b>
// <P>
// You can set the default system-wide language in +link{group:server_properties,server.properties}
// by setting
// <code>script.defaultLanguage</code>:
// <pre>
//     script.defaultLangauge: "groovy"
// </pre>
// Alternatively, anywhere a scriptlet is allowed, you can use the "language" attribute to declare
// the language.  For example:
// <pre>
//    &lt;operationBindings&gt;
//       &lt;operationBinding operationType="add"&gt;
//           &lt;script language="groovy"&gt;
//              ... Groovy code ...
//           &lt;/script&gt;
//       &lt;/operationBinding&gt;
//    &lt;/operationBindings&gt;
// </pre>
// <h3>Error Reporting</h3>
// <P>
// If your scriptlet crashes, this is reported in the server-side log along with the line number
// of the crash.
// <pre>
//      &lt;!-- crash will be reported at line 1 --&gt;
//      &lt;script&gt;crash()&lt;/script&gt;
//
//      &lt;!-- crash will be reported at line 2 --&gt;
//      &lt;script&gt;
//          crash()
//      &lt;/script&gt;
// </pre>
// It's common practice to use a CDATA tag so that XML-special characters such as &lt; do not have to be
// quoted as &amp;lt;.  When doing this, be aware that the line numbering will still start <b>from the
// &lt;script&gt; tag</b>, not the CDATA tag.  For example:
// <pre>
//      &lt;!-- crash will be reported at line 3 --&gt;
//      &lt;script&gt;
//          &lt;![CDATA[
//             5 < crash()
//          ]]&gt;
//      &lt;/script&gt;
//
//      &lt;!-- crash will be reported at line 2 --&gt;
//      &lt;script&gt;&lt;![CDATA[
//           5 < crash()
//      ]]&gt;&lt;/script&gt;
// </pre>
// <P>
// <h3>Java Imports</h3>
// <P>
// You can import Java libraries by placing a &lt;scriptImport&gt; tag immediately before a
// &lt;script&gt; or &lt;serverCondition&gt; tag, like so:
// <pre>
//     &lt;scriptImport&gt;javax.servlet.http.*&lt;/scriptImport&gt;
//     &lt;script language="groovy"&gt;
//         String sessionId = session.getId();
//         ...
// </pre>
// There is also a system-wide set of default imports:
// <pre>
// java.util.*
// javax.servlet.http.*
// com.isomorphic.base.Config
// com.isomorphic.util.*
// com.isomorphic.datasource.*
// com.isomorphic.rpc.RPCManager
// </pre>
// You can override these in +link{group:server_properties,server.properties} via the property
// <code>script.defaultImports</code>, which takes a comma- or space-separated list of packages or
// classes (like the above).
// <P>
// Dynamic languages such as Groovy or JavaScript allow you to place an import inside the script
// itself as well.
// <P>
// <h3>Available Languages</h3>
// <P>
// The Oracle JDK and JRE include support for JavaScript scripting via the Rhino engine.
// Alternatively, you may wish to consider +externalLink{http://code.google.com/p/jav8/,jav8},
// which is a Javascript JSR223 scripting implementation based on the V8 engine, giving
// Javascript execution speeds comparable to Google Chrome.
// <p>
// For convenience, SmartClient also bundles a .jar providing Groovy support from
// +externalLink{http://www.groovy-lang.org/}, which uses the Apache license.  We also include
// a .jar file providing Java language support (however see below for limitations).  This
// implementation is based on the BSD-licensed
// +externalLink{http://java.net/projects/scripting/, Java.net} implementation, but enhanced by
// Isomorphic to work around container-specific classloader issues that arise when running Java
// language scripting inside a servlet container and trying to reference common objects of the
// servlet API itself.  See +link{sunNotice} for licensing information.
// <P>
// There are <b>many</b> other languages available, sometimes with multiple implementations, and
// they are best found via web search.
// <P>
// <h3>Standard Headers &amp; Footers</h3>
// <P>
// You can define system-wide headers and footers for each language - code that is added before
// and after scriptlets wherever it is defined, and can set up variables or functions you use
// often.  To define the location of header and footer files, set
// <code>script.<i>languageName</i>.header</code> and <code>script.<i>languageName</i>.footer</code>
// in server.properties.  For example, these settings:
// <pre>
//   script.groovy.header: $webRoot/shared/header.groovy
//   script.groovy.footer: $webRoot/shared/footer.groovy
// </pre>
// would add the Groovy fragments found in header.groovy and footer.groovy to beginning and end of every
// scriptlet that declares language="groovy" (or declares no language if the default engine is
// "groovy").
// <P>
// <b>NOTE</b>: most scripting engines are available under several language names.  For example,
// the Rhino JavaScript engine registers both "javascript" and "ecmascript" as well as a few
// variations on letter case.  When using the "language" attribute on script tags, the exact value
// supplied is used to look up header and footer files via server.properties.  This means a
// language setting of "javascript" will find different header and footer files from a language of
// "JavaScript" even though both will execute via Rhino.
// <P>
// <h3>Java scriptlets and the default script wrapper</h3>
// <P>
// Although it's not usually considered a "scripting language", using the Java language for
// scriplets has the advantage that developers do not need to understand two languages in order to
// modify server-side code.  However, using Java for scripting presents special challenges,
// because unlike true scripting languages, in Java a piece of code cannot be compiled unless it
// forms a valid class definition.
// <P>
// For this reason, by default every Java scriplet has an implicit wrapper added around it which
// makes it into a class definition of a trivial class with one method, and your scriptlet code
// forms the body of that method, after a series of local variables have been set up to allow
// convenient access to context variables.  The header and footer files you've defined, if any,
// appear before and after your scriptlet, still within the method body.
// <P>
// This makes Java viable as a scripting language despite its verbosity - if the actual business
// logic to be executed consists of just a few lines of Java, your overall scriptlet will be only
// that long instead of being forced to contain a complete class definition.
// <P>
// The automatic wrapping of Java code can be disabled by setting
// <code>script.java.useDefaultScriptWrapper</code> to false in server.properties.  In this case
// any scriptlet must contain a valid class definition like the below - context variables need to
// be manually retrieved from the ScriptContext object instead of being automatically
// available as local variables, and the attribute "evalResult" is used to return data in lieu of
// using a <code>return</code> statement.
// <P>
// <pre>
// class Temp {
//     private static ScriptContext ctx;
//     public static void setScriptContext(ScriptContext context) {
//         ctx = context;
//     }
//     public static void main(String[] args) {
//         String result = "Hello World!";
//         ctx.setAttribute("evalResult", result,
//                          ScriptContext.ENGINE_SCOPE);
//     }
// }
// </pre>
// All scriptlets must also import javax.script.ScriptContext.  For obvious reasons setting
// <code>useDefaultScriptWrapper</code> to false is not recommended.
// <P>
// <h3>Returning values and JavaScript</h3>
// <P>
// Scriptlets written in Java <b>must</b> use a <code>return</code> statement to return a
// result from the scriptlet.  Scriptlets written in JavaScript <code>must not</code> use a
// <code>return</code> as Rhino will report this as an error - the JavaScript code is not
// executed in the scope of a function, and only functions can <code>return</code>.
// <P>
// Instead, JavaScript scriptlets should simply end with a statement indicating the value
// they would like to return.  For example:
// <pre>
//     // if used as the last line, the scriptlet
//     // returns the result of dsRequest.execute();
//     dsRequest.execute();
//
//     // if you already have the value as a variable,
//     // just end with the variable name plus semicolon
//     var dsResponse = dsRequest.execute();
//     dsResponse;
//
//     // add a line like this to force returning null
//     // instead of the result of the previous line of code
//     null;
// </pre>
// Groovy makes the <code>return</code> statement optional, and like JavaScript, will take the
// value of the last statement as the returned value if there is no explicit
// <code>return</code>.
// <P>
// Other languages supported by JSR223 may have other special semantics for returning data - see
// their documentation for details.
// <P>
// <h3>Available context variables for scriptlets</h3>
// <P>
// Context variables that are available to a scriptlet are explained in the documentation for the
// particular property where a scriptlet may be declared, for example,
// +link{operationBinding.script} and +link{validator.serverCondition}.
// <P>
// In most JSR223 languages, context variables are available as ordinary local variables and you
// can simply refer to them directly in your scriptlet.  This includes Java, so long as
// useDefaultScriptWrapper is left in it's default setting (see above).
//
// @title Server Scripting
// @treeLocation Concepts
// @example scriptingUserSpecificData
// @example scriptingValidation
// @visibility external
//<

//> @groupDef sunNotice
// The support provided in SmartClient for the use of Java as a server-side scripting language
// is based on Sun's "java-engine" implementation.  The license for that software requires
// that we reproduce the copyright notice and disclaimer in our documentation:
// <pre>
//  Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved.
//  Use is subject to license terms.
//
//  Redistribution and use in source and binary forms, with or without modification, are
//  permitted provided that the following conditions are met: Redistributions of source code
//  must retain the above copyright notice, this list of conditions and the following disclaimer.
//  Redistributions in binary form must reproduce the above copyright notice, this list of
//  conditions and the following disclaimer in the documentation and/or other materials
//  provided with the distribution. Neither the name of the Sun Microsystems nor the names of
//  is contributors may be used to endorse or promote products derived from this software
//  without specific prior written permission.
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
//  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
//  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
//  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
//  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
//  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
//  POSSIBILITY OF SUCH DAMAGE.
// </pre>
//
// @title Sun's java-engine implementation - Notice and Disclaimer
// @treeLocation Concepts
// @visibility external
//<

//> @groupDef nonJavaBackend
// While SmartClient's extensive server features are lost if you cannot install the Java-based
// server framework, SmartClient UI components can be integrated with any server technology.
// This topic provides pointers to documentation that is most relevant for this type of
// integration.
// <p>
// <smartgwt>
// If you are using a non-Java backend such as PHP or .NET, you may want to consider
// SmartGWT's sister product &#83;martClient, which has the same functionality but allows
// programming in JavaScript instead of Java.  However if you prefer to use Java on the client,
// this is fine - SmartGWT and SmartClient have identical capabilities for integrating with
// non-Java servers.
// <p>
// When run on a SmartGWT project, the GWT compiler produces ordinary web assets - JavaScript
// files, .html files, images, etc - which can be deployed on any web server like any other web
// assets.  How best to integrate GWT into your development workflow is outside the scope of
// this documentation, however, consider using the SmartGWT server-side HttpProxy (mentioned
// below) as a means of using a Java IDE to develop GWT Java code in the usual way, while
// allowing your GWT application to contact services hosted on another server (which is
// running your PHP, .NET or other server code).  The HttpProxy can then be eliminated from
// your final deployment by deploying the web assets produced by GWT directly on the server
// running your non-Java server code.
// </smartgwt>
// <smartclient>
// <h4>Installation</h4>
// <P>
// As described in +link{group:iscInstall, <i>Deploying SmartClient</i>}, for a client-only
// integration, installation consists of just copying a directory of JavaScript and media files
// to your webserver.
// <P>
// <h4>Creating Components</h4>
// <P>
// SmartClient components can be included in any .html page, including dynamically generated pages
// produced by .php or .asp files.  The SmartClient libraries can be included in the page as
// follows:
// <pre>
// &lt;HTML&gt;&lt;HEAD&gt;
// &lt;SCRIPT&gt;var isomorphicDir="../isomorphic/";&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_Core.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_Foundation.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_Containers.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_Grids.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_Forms.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/system/modules/ISC_DataBinding.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=../isomorphic/skins/SmartClient/load_skin.js&gt;&lt;/SCRIPT&gt;
// &lt;/HEAD&gt;&lt;BODY&gt;
// ...</pre>
// SmartClient components can then be created via normal JavaScript:
// <pre>
// &lt;SCRIPT&gt;
// isc.Button.create({
//     title:"Button",
//     click:"isc.say('Hello World')"
// });
// &lt;/SCRIPT&gt;
// </pre>
// This approach is discussed in more detail in the
// +externalLink{/docs/SmartClient_Quick_Start_Guide.pdf,QuickStart Guide}, Chapter 4, <i>Coding</i>.
// Note that JavaScript-based component instantiation is currently the recommended approach, and
// most examples are provided in the JavaScript format.
// </smartclient>
// <P>
// <h4>Data Loading / Data Binding</h4>
// <P>
// The primary focus of SmartClient integration is connecting DataSource operations to your
// server.  The +link{group:clientDataIntegration,Client-side Data Integration} chapter covers
// the key approaches, including cookbook approaches for REST-based integration with any server
// that can return XML or JSON over HTTP.
// <P>
// <h4>Simple RPCs (non-DataSource requests)</h4>
// <P>
// You can implement simple RPCs by having your server output
// +externalLink{http://www.json.org/,JSON} (JavaScript Object Notation) and using
// +link{RPCRequest.evalResult} to directly turn JSON results into live JavaScript objects.
// +link{RPCRequest.serverOutputAsString} lets you load arbitrary server results, including
// JSON results that need to be processed before they can be eval()'d.
// <P>
// Alternatively, if you are familiar with WSDL web services, you can implement simple RPCs as
// web service operations: use +link{XMLTools.loadWSDL()} to load the service definition, and
// then use +link{WebService.callOperation()} to call the operations.  We don't generally
// recommend this approach unless you are already deeply familiar with WSDL - it's far more
// complicated that producing and consuming JSON.
// <P>
// <h4>HTTPProxy: Cross-site or cross-port data loading</h4>
// <P>
// If you develop a prototype using the SmartClient SDK and SmartClient Java Server, and then
// you migrate the prototype to another server technology, you need to be aware that the
// SmartClient Java Server includes an HTTPProxy servlet that allows SmartClient interfaces to
// contact servers other than the origin server (bypassing what is called the
// +externalLink{http://www.google.com/search?q=same+origin+policy,"same origin policy"}).
// <P>
// SmartClient uses the HttpProxy automatically when needed, so it may not be obvious that the
// HTTPProxy is in use.  Then, your migrated application will encounter errors attempting to
// contact the HTTPProxy servlet.
// <P>
// To avoid these errors, ensure that all services that your application uses are accessed
// using the same hostname and port as the page was loaded from.  In particular, watch for WSDL
// files, which contain the service URL - you may need to use +link{webService.setLocation()}
// to ensure that the web service URL and page URL match.
// <P>
// If your production application really does need to access services or content hosted on
// other servers, typical practice is to pursue normal SmartClient integration with your
// server, then write server-side code that contacts other hosts on behalf of your SmartClient
// interface.
//
// @visibility xmlBinding
// @treeLocation Concepts
// @title .NET, PHP, Serverless Integration
//<

// JSP tag docs
// ---------------------------------------------------------------------------------------

//> @groupDef jspTags
//
// The SmartClient Java Server component ships with a number of custom JSP tags designed to
// make development with SmartClient easier.  The custom tags are defined in
// <code>[webroot]/WEB-INF/iscTaglib.xml</code> in the SDK package.  To make use of these tags, make
// sure you have the following entry in your web.xml (the SDK already contains this entry by
// default):<br>
// <pre>
// &lt;taglib&gt;
//     &lt;taglib-uri&gt;isomorphic&lt;/taglib-uri&gt;
//     &lt;taglib-location&gt;/WEB-INF/iscTaglib.xml&lt;/taglib-location&gt;
// &lt;/taglib&gt;
// </pre>
// <p>
// Also, the JSP page making use of the tag must load the custom taglib via the following
// directive:<br>
// <pre>
// &lt;%@ taglib uri="/WEB-INF/iscTaglib.xml" prefix="isomorphic" %&gt;
// </pre>
// All SmartClient JSP tags produce either HTML or JavaScript output, so you can easily see
// what any given tag is generating by doing a "View->Source" in your browser after browsing to
// the JSP that contains your tag.  Tags that produce HTML must be located in the HTML BODY
// context in your JSP - that is, outside of any <code>&lt;SCRIPT&gt;</code> tags and inside
// <code>&lt;BODY&gt;</code> tags.  Tags that produce JavaScript must be located inside
// <code>&lt;SCRIPT&gt;</code> tags.
//
// @visibility external
// @requiresModules SCServer
// @title SmartClient JSP Tags
//<

//> @groupDef loadISCTag
//
// <i>produces:</i> HTML
// <p>
// This tag sets window.isomorphicDir and loads a default set of SmartClient modules and a
// skin.  SmartClient modules are JavaScript files that need to be loaded via
// <code>&lt;SCRIPT SRC=/some/filename.js&gt;&lt;/SCRIPT&gt;</code>
// blocks as part of the loading HTML page.  The modules themselves are stored in the
// <code>[webroot]/isomorphic/system/modules</code> directory.  A skin defines the look and feel of your
// website and typically consists of at least one CSS file and some SmartClient UI Component
// default overrides.  Skins are located in <code>[webroot]/isomorphic/skins</code>.  Loading a skin
// consists of loading the corresponding <code>load_skin.js</code> - residing at top-level in
// each skin directory.
// <p>
// The default set of modules loaded by this tag are: <code>Core, Foundation, Containers,
// Grids, Forms, DataBinding</code>.  The default skin is the <code>SmartClient</code> skin.
// So, for example inserting this into your JSP:
// <pre>
// &lt;isomorphic:loadISC/&gt;
// </pre>
// Produces the following output:
// <pre>
// &lt;SCRIPT&gt;window.isomorphicDir='isomorphic/';&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_Core.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_Foundation.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_Containers.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_Grids.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_Forms.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/modules/ISC_DataBinding.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT src=isomorphic/skins/SmartClient/load_skin.js&gt;&lt;/SCRIPT&gt;
// </pre>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>skin</b><br>
// <i>value format</i>: Name of skin to load.  See the <code>[webroot]/isomorphic/skins</code> directory
// for a list of available skins.  You can also create your own skin by copying an existing
// skin directory recursively, modifying anything you need, and using your new skin name as the
// value here.<br>
// <i>default value</i>: "SmartClient"
// <p>
// This selects a skin other than the default.  The value of this attribute is used as a path
// component to <code>load_skin.js</code>.  For example, specifying:
// <pre>
// &lt;isomorphic:loadISC skin="standard"/&gt;
// </pre>
// Produces the following <code>load_skin.js</code> directive (in addition to all the standard
// module loads):
// <pre>
// &lt;SCRIPT src=isomorphic/skins/standard/load_skin.js&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// <b>skinDir</b><br>
// <i>value format</i>: path or URL up to and including the name of the skin<br>
// <i>default value</i>: automatically derived from the value of the <code>skin</code>
// attribute
// <p>
// This attribute is usable in lieu of the <code>skin</code> attribute and allows you to
// specify the full path or URL to the directory containing the load_skin.js for your skin.
// This allows you to move the directory containing the skin anywhere you want - including to a
// completely different server.
// <p>
// <b>includeModules</b><br>
// <i>value format</i>: Comma separated list of additional modules to load.  See the
// <code>[webroot]/isomorphic/system/modules</code> directory for a list of loadable modules.  The
// leading "ISC_" is optional, spaces after commas are also optional.<br>
// <i>default value</i>: NONE
// <p>
// This attribute allows you to specify SmartClient modules that you want to load in addition
// to the default set of modules. For example, specifying:
// <pre>
// &lt;isomorphic:loadISC includeModules="Analytics"/&gt;
// </pre>
// Produces the following output:
// <pre>
// &lt;SCRIPT&gt;window.isomorphicDir='isomorphic/';&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Core.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Foundation.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Containers.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Grids.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Forms.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_DataBinding.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT SRC=isomorphic/system/development/ISC_Analytics.js&gt;&lt;/SCRIPT&gt;
// &lt;SCRIPT src=isomorphic/skins/SmartClient/load_skin.js&gt;&lt;/SCRIPT&gt;
// </pre>
// Notice the addition of the line that loads <code>ISC_Analytics.js</code>.  SmartClient
// modules must be loaded in a particular order and may have dependencies on other modules.
// The <code>loadISC</code> automatically resolves any missing dependencies and includes them
// in the list of modules to load.  The list of modules to load is also automatically reordered
// as required by this tag, so you can specify just the modules you care about, in any order,
// and <code>loadISC</code> will do the right thing.
// <p>
// <b>modules</b><br>
// <i>value format</i>: Comma separated list of modules to load.  See the
// <code>[webroot]/isomorphic/system/modules</code> directory for a list of loadable modules.  The
// leading "ISC_" is optional, spaces after commas are also optional.<br>
// <i>default value</i>: "Core, Foundation, Containers, Grids, Forms, DataBinding"
// <p>
// Specifying this attribute overrides the default set of modules loaded by
// <code>loadISC</code>. This is useful if you don't need to load all of the default modules on
// a particular page.
// <p>
// <b>isomorphicURI</b><br>
// <i>value format</i>: absolute or relative (from current URI) path to the
// <code>isomorphic</code> directory (by default, located in webRoot).<br>
// <i>default value</i>: automatically derived
// <p>
// This value sets <code>window.isomorphicDir</code> and defines the base path to SmartClient
// modules from the currently executing JSP.  By default, this value is automatically derived
// and you don't need to override it.  Generally, an override is only required if you're
// pointing multiple web application deployments at one base URL for cache coherence reasons.
// <p>
// <b>defer</b><br>
// <i>value format</i>: boolean - acceptable values: "true" or "false"<br>
// <i>default value</i>: "false"
// <p>
// This attribute applies only if you're using the optional Network Performance component of
// SmartClient.  By default, the modules specified by the <code>loadISC</code> tag are loaded
// synchronously - that is, the browser waits for each module to download before rendering
// any HTML on the page.  Specifying <code>defer="true"</code> makes the module load
// asynchronous.  In this mode, all the page content except the modules specified by
// <code>loadISC</code> is loaded and rendered first, and then the SmartClient modules are
// loaded.  The asynchronous mode makes it possible to render some content and allow the user
// to interact with the site immediately, while SmartClient modules load in the background.
// <p>
// Note that typically SmartClient modules are delivered compressed with cache headers, so the
// download delay really only affects first time users of your site, those whose cached
// versions have expired, or if you've upgraded your SmartClient module to a newer version that
// the user has cached.
// <p>
// Using this directive is equivalent to calling +link{FileLoader.loadISC()}.
// <p>
// <b>cacheOnly</b><br>
// <i>value format</i>: boolean - acceptable values: "true" or "false"<br>
// <i>default value</i>: "false"
// <p>
// This attribute applies only if you're using the optional Network Performance component of
// SmartClient.  This works like <code>defer</code> (see above), except that the modules are
// only loaded into the browser cache, but not executed.  This means that when the specified
// modules download, they'll be added to the browser cache, but will not be available to the
// execution context of the page.  Actually executing the module code takes some processing
// time on the client, whereas simply caching it does not.  The typical use case for this
// directive is on some intermediary page before the user gets to a SmartClient-enabled page.
// For example, you could include a cacheOnly <code>loadISC</code> tag on a login page that
// does not use SmartClient components, but protects a SmartClient-enabled application.  While
// the user is typing in their credentials, the SmartClient modules will cache in the
// background, then once the user logs in and is sent to a SmartClient-enabled page, the
// modules are simply read from cache.
// <p>
// Using this directive is equivalent to calling +link{FileLoader.cacheISC()}.
// <p>
// <b>onload</b><br>
// <i>value format</i>: String - any javascript code<br>
// <i>default value</i>: NONE
// <p>
// This attribute applies only if you're using the optional Network Performance component of
// SmartClient.  This attribute is usable in conjunction with <code>defer</code> and
// <code>cacheOnly</code> attributes.  This attribute allows you to specify a callback to
// execute when loading or caching of the specified modules has completed.  You can use this
// to, for example, replace portions of the page with SmartClient components once SmartClient
// has loaded.
// <p>
// The value of this attribute is passed as the <code>onload</code> handler to the generated
// +link{FileLoader.cacheISC()} or +link{FileLoader.loadISC()} call as appropriate.
// <p>
// <b>useSimpleNames</b><br>
// <i>value format</i>: boolean - acceptable values: "true" or "false"<br>
// <i>default value</i>: "true"
// <p>
// Setting this attribute to false puts SmartClient in namespace mode.  See +link{class:isc}
// for more info.
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadISC&gt;
//<


//> @groupDef loadModulesTag
//
// <i>produces:</i> HTML
// <p>
// This tag works just like +link{group:loadISCTag} except it does not load a skin.  All other
// attributes are supported on this tag just as on <code>loadISC</code>.  This tag is useful if
// you have a single "header" JSP that has the <code>loadISC</code> tag that you then include in other
// SmartClient-enabled JSPs that require additional modules.  The JSPs that require additional
// modules can then use the <code>loadModules</code> to load additional SmartClient modules.
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadModules&gt;
//<


//> @groupDef loadDSTag
//
// <i>produces:</i> JavaScript
// <p>
// This tag converts a SmartClient DataSource or SimpleType defined in XML to JavaScript for
// use in databinding on the client (browser).
// <p>
// Note that this JSP tag must be surrounded by &lt;SCRIPT&gt; tags in the JSP because it
// generates JavaScript code.  Like other tags that generate JavaScript code, this tag can be
// used in a JSP that is included from your main page in order to create separate
// cacheability.  For example:
// <pre>
// &lt;SCRIPT SRC="myDataSources.jsp"&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>ID</b><br>
// <i>value format</i>: String - ID of datasource or simpleType to load<br>
// <i>default value</i>: NONE
// <p>
// This attribute specifies the name of the dataSource or simpleType that you wish to load.
// DataSources are located in <code>[webroot]/shared/ds</code> by default.  This location is
// changeable in <code>[webroot]/WEB-INF/classes/server.properties</code> by setting the config
// parameter <code>project.datasources</code> to the directory where your dataSources are
// located.
// We recommend that for prototyping, at least, you use the default directory.
// <p>
// For example:
// <pre>
// &lt;isomorphic:loadDS ID="supplyItem"/&gt;
// </pre>
// Would load the <code>supplyItem</code> DataSource.
// <p>
// You can also load multiple dataSources in one go by specifying a comma-separated list of
// dataSource names as the ID.  For example:
// <pre>
// &lt;isomorphic:loadDS ID="supplyItem, employees, worldDS"/&gt;
// </pre>
// See +link{group:dataSourceDeclaration} for more details on creating DataSources and an
// example.
// <p>
// See +link{class:SimpleType} for more details on how to define server-side SimpleType in
// xml format.
// <p>
// <b>name</b><br>
// <i>value format</i>: String - ID of datasource to load<br>
// <i>default value</i>: NONE
// <p>
// This is a synonym for the <code>ID</code> attribute.
//
//
// @see group:dataSourceDeclaration
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadDS&gt;
//<


//> @groupDef loadUITag
//
// <i>produces:</i> JavaScript
// <p>
// This tag converts SmartClient UI components declaratively specified in an XML "UI" file to
// JavaScript for execution in the browser.
// <p>
// The XML->JS translation works just like with +link{group:xmlTag}, except the XML is read
// from an external file instead of from the body of the tag.
// <p>
// Note that this JSP tag must be surrounded by &lt;SCRIPT&gt; tags in the JSP because it
// generates JavaScript code.  Like other tags that generate JavaScript code, this tag can be
// used in a JSP that is included from your main page in order to create separate
// cacheability.  For example:
// <pre>
// &lt;SCRIPT SRC="myUIDefinitions.jsp"&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>name</b><br>
// <i>value format</i>: String - name of UI file to load (minus extension)<br>
// <i>default value</i>: NONE
// <p>
// This attribute specifies the name of the file that contains the UI components to translate.
// UI files are located in <code>[webroot]/shared/ui</code> by default.  This location is
// changeable in <code>[webroot]/WEB-INF/classes/server.properties</code> by setting the config
// parameter <code>project.ui</code> to the directory where your UI files are located.
// We recommend that for prototyping, at least, you use the default directory.
// <p>
// For example:
// <pre>
// &lt;isomorphic:loadUI name="test"/&gt;
// </pre>
// Would translate declarative XML in the file <code>[webroot]/shared/ui/test.ui.xml</code> to
// JavaScript and output the results into the JSP output stream at the location of the tag.
//
// @see group:xmlTag
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadUI&gt;
//<

//> @groupDef xmlTag
//
// <i>produces:</i> JavaScript
// <p>
// This tag converts SmartClient UI components declaratively specified in the body of the tag to
// JavaScript for execution in the browser.
// <p>
// The XML->JS translation works just like with +link{group:loadUITag}, but the XML is read
// from the body of the tag.  If you wish, you can also specify an external filename, and XML
// will be read from that file, in addition to any XML encountered in the body of the tag.  If
// you do specify that an external file should be read, it is read from a path starting in
// your webroot (as opposed to the <code>&lt;loadUI&gt;</code> tag, which looks in
// <code>shared/ui</code>)
//
// <p>
// Note that this JSP tag must be surrounded by &lt;SCRIPT&gt; tags in the JSP because it
// generates JavaScript code.  Like other tags that generate JavaScript code, this tag can be
// used in a JSP that is included from your main page in order to create separate
// cacheability.  For example:
// <pre>
// &lt;SCRIPT SRC="myUIDefinitions.jsp"&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// Example of using this tag :
// <pre>
// &lt;isomorphic:XML&gt;
// &lt;Canvas&nbsp;backgroundColor="black"/&gt;
// &lt;/isomorphic:XML&gt;
// </pre>
// Would output the following JavaScript code:
// <pre>
// Canvas.create({
//   backgroundColor: "black"
// });
// </pre>
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>filename</b><br>
// <i>value format</i>: String - name of XML file to load (including the "XML" extension)<br>
// <i>default value</i>: NONE
// <p>
// This optional attribute specifies the name of an XML file to read and convert.
// <p>
// An example that specifies both a filename and some XML in the tag body:
// <pre>
// &lt;isomorphic:XML filename="test.xml"&gt;
//   &lt;Canvas backgroundColor="red"/&gt;
// &lt;/isomorphic:XML&gt;
// </pre>
//
// @see loadUITag
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:XML&gt;
//<

//> @groupDef loadXMLSchemaTag
//
// <i>produces:</i> JavaScript
// <p>
// Load an XML Schema (.xsd) file and create a +link{SchemaSet} object representing the loaded
// definitions. This tag works just like +link{XMLTools.loadXMLSchema()}, except it's
// synchronous and the result is server-cacheable.
// <p>
// Note that this JSP tag must be surrounded by &lt;SCRIPT&gt; tags in the JSP because it
// generates JavaScript code.  Like other tags that generate JavaScript code, this tag can be
// used in a JSP that is included from your main page in order to create separate
// cacheability.  For example:
// <pre>
// &lt;SCRIPT SRC="myXMLSchemaDefinitions.jsp"&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>url</b><br>
// <i>value format</i>: URL or URI
// <i>default value</i>: NONE
// <p>
// This attribute specifies the URL or URI of the XML Schema file to fetch and translate.  This
// can be either a remote URL - e.g: <code>http://host:port/schemaFile.xsd</code> or a relative
// or absolute URI to a file local to this container - e.g: <code>/some/schemaFile.xsd</code>
// or <code>../some/schemaFile.xsd</code>.  If the url is a remote URL, then an HTTP request
// will be made for the file.  If it is local, it will be fetched from disk using standard
// Servlet APIs (<code>ServletContext.getResourceAsStream()</code>).
// <p>
// <b>cache</b><br>
// <i>value format</i>: Integer (number of seconds to cache result)
// <i>default value</i>: 3600 (1 hour)
// <p>
// This attribute specifies the number of seconds for which the fetched XML Schema is cacheable
// on the server.  Fetching an XML Schema file from a remote server can cause a significant
// delay in JSP processing, and XML Schema files rarely change outside of a development
// environment.  Set this value to zero to disable caching.
//
// @see XMLTools.loadXMLSchema()
//
// @visibility xmlBinding
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadXMLSchema&gt;
//<

//> @groupDef loadWSDLTag
//
// <i>produces:</i> JavaScript
// <p>
// Load a WSDL file and create an instance of +link{WebService} that allows invoking operations
// and binding DataSources to web service operations. This tag works just like
// +link{XMLTools.loadWSDL()}, except it's synchronous and the result is server-cacheable.
// <p>
// Note that this JSP tag must be surrounded by &lt;SCRIPT&gt; tags in the JSP because it
// generates JavaScript code.  Like other tags that generate JavaScript code, this tag can be
// used in a JSP that is included from your main page in order to create separate
// cacheability.  For example:
// <pre>
// &lt;SCRIPT SRC="myWebServiceDefinition.jsp"&gt;&lt;/SCRIPT&gt;
// </pre>
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>url</b><br>
// <i>value format</i>: URL or URI
// <i>default value</i>: NONE
// <p>
// This attribute specifies the URL or URI of the WSDL file to fetch and translate.  This can
// be either a remote URL - e.g: <code>http://host:port/wsdlFile.wsdl</code> or a relative or
// absolute URI to a file local to this container - e.g: <code>/some/wsdlFile.wsdl</code> or
// <code>../some/wsdlFile.wsdl</code>.  If the url is a remote URL, then an HTTP request will
// be made for the file.  If it is local, it will be fetched from disk using standard Servlet
// APIs (<code>ServletContext.getResourceAsStream()</code>).
// <p>
// <b>cache</b><br>
// <i>value format</i>: Integer (number of seconds to cache result)
// <i>default value</i>: 3600 (1 hour)
// <p>
// This attribute specifies the number of seconds for which the fetched WSDL is cacheable on
// the server.  Fetching a WSDL file from a remote server can cause a significant delay in JSP
// processing, and WSDL files rarely change outside of a development environment.  Set this
// value to zero to disable caching.
//
// @see group:wsdlBinding
// @see XMLTools.loadWSDL()
//
// @visibility xmlBinding
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadWSDL&gt;
//<

//> @groupDef jsStringTag
//
// <i>produces:</i>JavaScript
// <p>
// This tag takes everything in its body and outputs a correctly-escaped JavaScript string.
// This is useful for capturing HTML for display in a Canvas or Label, for example.
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>var</b><br>
// <i>value format</i>: Any legal JavaScript variable name
// <i>default value</i>: NONE
// <p>
// If specified, the escaped string is assigned to a newly created variable of the specified
// name.  e.g: var foo = "bar";
// <p>
// <b>filename</b><br>
// <i>value format</i>: webRoot-relative path to file
// <i>default value</i>: NONE
// <p>
// If specified, the resulting string content is loaded from the specified file instead of
// from the tag body.
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:jsString&gt;
//<



//> @groupDef xmlClientVsServer
// Server benefits
// - faster client-side processing
// Server neutral
// - heavy customization of XML transform, if any, written in Java
//
// Client benefits
// - faster server-side processing
// Client neutral
// - heavy customization of XML transform, if any, written in JavaScript
//<


//> @class WebService
// Class representing a WebService definition derived from a WSDL file.
// <P>
// A Web Service object allows you to invoke operations (via
// +link{method:WebService.callOperation(),callOperation()}), inspect schema declared in the
// WSDL file (+link{method:WebService.getSchema(),getSchema()}), and perform simple read-only
// databinding +link{method:WebService.getFetchDS()}.
// <P>
// Once a WebService has been loaded, a DataSource can be declared with a
// +link{attr:DataSource.serviceNamespace} to connect it to the web service, allowing DataSource
// data to be loaded and saved to the web service using
// +link{class:OperationBinding,operationBindings}.
//
// @group webService
// @treeLocation Client Reference/Data Binding
// @visibility xmlBinding
//<


isc.defineClass("Schema", "DataSource").addProperties({
    dataFormat : "xml",

    // since the schema already knows the types and can be consulted for type information at
    // any time, when decoding data we drop all namespace declarations on nodes.
    dropNamespaceDeclarations:true,

    // NOTE: currently all subclasses of Schema are generated from various XML formats.  We
    // assume they shouldn't be global variables, which is really just intended as a
    // convenience for user-authored DataSources.
    addGlobalId:false
});

isc.defineClass("WSDLMessage", "Schema").addMethods({


    getWSOperation : function (dsRequest) {
        var service = this.getWebService(dsRequest);
        // being invoked by way of an entity DataSource performing an operation
        if (dsRequest && dsRequest.wsOperation) return service.getOperation(dsRequest.wsOperation);
        // being invoke standalone, eg, callOperation()
        else return service.getOperationForMessage(this.ID.substring(8));
    }
});

isc.defineClass("XSElement", "Schema");
isc.defineClass("XSComplexType", "Schema");

//> @class SchemaSet
// A set of schema derived from the &lt;xsd:schema&gt; element in a WSDL or XML schema file
// loaded by +link{XMLTools.loadWSDL()} or +link{XMLTools.loadXMLSchema()}.
//
// @treeLocation Client Reference/Data Binding
// @visibility xmlBinding
//<
isc.defineClass("SchemaSet").addMethods({
    //> @attr schemaSet.schemaNamespace (URI : null : R)
    // Namespace of this SchemaSet, derived from the <code>targetNamespace</code>
    // attribute of the <code>&lt;schema&gt;</code> element.
    //
    // @group webService
    // @visibility xmlBinding
    //<
    init : function () {
        this.ns.ClassFactory.addGlobalID(this);

        // register the schemaSet globally with the SchemaSet class
        var schemaNamespace = this.schemaNamespace,
            registry = isc.SchemaSet.schemaSets,
            existingSchema = registry[schemaNamespace];

        // an xs:schema that contains only an xs:import generates an empty SchemaSet.  Don't
        // clobber an existing, non-empty schemaset with an empty version of the schemaset
        // loaded later
        if (existingSchema == null ||
            // empty existing schemaset
            ((existingSchema.schema == null && existingSchema.schema.length == 0) &&
            // non-empty new schemaset
            (this.schema != null && this.schema.length != 0)))
        {
            registry[schemaNamespace] = this;
        }

        // index all schema within this schemaset
        var serviceNamespace = this.serviceNamespace;
        if (this.schema) {
            this._typeIndex = {};
            this._elementIndex = {};
            this._simpleTypeIndex = {};

            for (var i = 0; i < this.schema.length; i++) {
                var schema = this.schema[i];
                // ensure all schema that belong to this set have their schemaNamespace
                // attribute set
                schema.serviceNamespace = serviceNamespace;
                schema.schemaNamespace = schemaNamespace;
                schema.location = this.location;

                // make an index of all the schema in this SchemaSet
                if (isc.isA.SimpleType(schema)) {

                    if (schema.inheritsFrom && schema.inheritsFrom == schema.name &&
                        schema.xmlSource == "XSElement") continue;
                    this._simpleTypeIndex[schema.name] = schema;
                } else if (schema.ID) {

                    if (isc.isAn.XSElement(schema)) {
                        this._elementIndex[schema.ID] = schema;
                    } else {
                        this._typeIndex[schema.ID] = schema;
                    }
                }
            }
        }
        // for loadXMLSchema() callback to return loaded SchemaSet
        isc.SchemaSet._lastLoaded = this;
    },

    //> @method schemaSet.getSchema() [A]
    // Get the schema definition of any complexType or element of complexType defined within
    // the &lt;schema&gt; element this SchemaSet represents.
    //
    // @param schemaName (String) name of the schema to retrieve
    // @param [schemaType] (String) optional type of schema to return, either "element" for
    //                              xs:element definitions only or "type" for xs:complexType
    //                              definitions.  If unspecified, either will be returned,
    //                              with types preferred if names collide
    // @return (DataSource) the schema if found, or null
    // @visibility xmlBinding
    // @example xmlSchemaImport
    //<
    getSchema : function (schemaName, schemaType, alreadyVisited) {
        // xs:schema routinely import each other, so avoid looping getSchema calls
        if (!alreadyVisited) alreadyVisited = [this];
        else alreadyVisited.add(this);

        var schema;

        // try local, type-specific indexes
        if (schemaType == isc.DS._$element) schema = this._elementIndex[schemaName];
        else if (schemaType == isc.DS._$type) schema = this._typeIndex[schemaName];

        // if schemaType wasn't specified take either type of schema locally
        if (schemaType == null) {
            schema = this._typeIndex[schemaName] || this._elementIndex[schemaName];
            if (schema != null) return schema;
        }

        // resolve all <xs:import> tags to find already loaded schemaSets we imported
        if (!this._lookedUpImports) {
            isc.SchemaSet.findLoadedImports(this);
            this._lookedUpImports = true;
        }

        // try imported schema if present
        var schemaSets = this._schemaSets;
        if (schemaSets != null) {
            for (var i = 0; i < schemaSets.length; i++) {
                var schemaSet = schemaSets[i];
                if (alreadyVisited.contains(schemaSet)) continue;
                schema = schemaSet.getSchema(schemaName, schemaType, alreadyVisited);
                if (schema != null) return schema;
            }
        }
    },

    getSimpleType : function (typeName, alreadyVisited) {
        // xs:schema routinely import each other, so avoid looping getSchema calls
        if (!alreadyVisited) alreadyVisited = [this];
        else alreadyVisited.add(this);

        var simpleType;
        if (this._simpleTypeIndex) {
            simpleType = this._simpleTypeIndex[typeName];
            if (simpleType) return simpleType;
        }

        if (this._schemaSets != null) {
            for (var i = 0; i < this._schemaSets.length; i++) {
                var schemaSet = this._schemaSets[i];
                if (alreadyVisited.contains(schemaSet)) continue;
                simpleType = schemaSet.getSimpleType(typeName, alreadyVisited);
                if (simpleType != null) return simpleType;
            }
        }
    },

    setLocation : function (location) {
        this.location = location;
        if (!this.schema) return;

        for (var i = 0; i < this.schema.length; i++) {
            var schema = this.schema[i];
            schema.location = location;
        }
    },

    loadImports : function (callback) {
        isc.SchemaSet.loadImports(callback, this);
    },

    // override point for changing how imported schema are loaded
    // NOTE: duplicated in WebService
    loadImport : function (namespace, location, callback, isWSDL) {
        return isc.SchemaSet.loadImport(namespace, location, callback, isWSDL, this);
    },
    doneImporting : function () {
        this.fireCallback(this._doneImportingCallback);
    },

    // when captureXML has been set in loadWSDL / loadXMLSchema, this method is called on the
    // initiator of a series of schema loads so that complete source is available
    addImportXMLSource : function (xmlText, location) {
        this.importSources = this.importSources || [];
        this.importSources.add({
            xmlText : xmlText,
            location : location
        })
    },

    addSchemaSet : function (schemaSet, namespace) {
        this._imports = this._imports || [];
        this._imports.add(schemaSet);
    }

});
isc.SchemaSet.addClassMethods({
    schemaSets : {},

    //> @classMethod SchemaSet.get() [A]
    // Retrieve a SchemaSet object by it's schemaNamespace.
    //
    // @param schemaNamespace (String) uri from the "targetNamespace" attribute of the
    // &lt;xsd:schema&gt; element from the XML Schema or WSDL file this SchemaSet was derived
    // from.
    // @return (SchemaSet) the requested SchemaSet, or null if not loaded
    //
    // @visibility xmlBinding
    //<
    get : function (schemaNamespace) {
        return this.schemaSets[schemaNamespace];
    },

    // find already loaded SchemaSets to fulfill <xs:import>s from within a WSDL or XML Schema
    // file.  Note loadImports() is what would actually attempt to load imported files from
    // the "location" attribute provided on the <xs:import> tag (if any)
    // "loader" may be an instance of WebService or SchemaSet
    findLoadedImports : function (loader) {
        var imports = this.getAllImports(loader);

        if (!imports) return;

        var schemaSets = loader._schemaSets = loader._schemaSets || [];
        var webServices = loader._webServices = loader._webServices || [];

        for (var i = 0; i < imports.length; i++) {
            var importDef = imports[i],
                isWSDL = importDef.isWSDL,
                importNamespace = importDef.namespace;

            if (this._ignoreImports.contains(importNamespace)) continue;

            // we already have it
            if ((isWSDL && webServices.find("serviceNamespace", importNamespace)) ||
                (!isWSDL && schemaSets.find("schemaNamespace", importNamespace)))
                continue;

            var importObj = isWSDL ?
                    isc.WebService.get(importNamespace) : isc.SchemaSet.get(importNamespace);
            if (importObj == null) {
                var preamble;
                if (isc.isA.WebService(loader)) {
                    preamble = "WebService with targetNamespace '" + loader.serviceNamespace;
                } else {
                    preamble = "SchemaSet with targetNamespace '" + loader.schemaNamespace;
                }
                // it's common for WSDL or XSD to contain a bunch of imports for which there is
                // no actual file, in a usage almost like "marker interfaces" in Java.  If
                // there's no location, don't consider this a warning.
                var logMethod = importDef.location ? "logWarn" : "logInfo";
                loader[logMethod](preamble + "' could not find " +
                                  (isWSDL ? "webService" : "SchemaSet") +
                                  " for namespace: '" + importNamespace +
                                  "'. Pass autoLoadImports to loadWSDL()/loadXMLSchema() or " +
                                  "separately load via loadWSDL/loadXMLSchema jsp tag or method",
                                   "schemaLoader");
                continue;
            }
            // any schemaSet that was independently loaded would have it's own location
            // already set.  If it's got no location assume it was loaded with this web
            // service.  XXX not true of schema loaded by JSP tags
            if (importObj.location == null) importObj.setLocation(loader.location);
            isWSDL ? webServices.add(importObj) : schemaSets.add(importObj);
        }
    },

    getAllImports : function (loader) {
        var imports = loader.schemaImports;
        if (loader.wsdlImports) {
            loader.wsdlImports.setProperty("isWSDL", true);
            imports = imports || [];
            imports = imports.concat(loader.wsdlImports);
        }
        return imports;
    },

    // fulfill <xs:import>s from within a WSDL or XML Schema by actually attempting to load
    // the imported XML Schema from the "location" attribute provided on the <xs:import> tag
    // (if any)
    loadImports : function (callback, loader) {
        loader._doneImportingCallback = callback;

        var imports = this.getAllImports(loader);

        // if nothing to import, fire callback immediately
        if (!imports) return loader.doneImporting();

        loader._importCount = 0;

        //loader.logWarn("imports are: " + loader.echoFull(imports));

        for (var i = 0; i < imports.length; i++) {
            var importDef = imports[i],
                namespace = importDef.namespace;

            if (namespace) {
                var alreadyLoaded = (importDef.isWSDL ?
                    isc.WebService.get(namespace) :
                    isc.SchemaSet.get(namespace));
                if (alreadyLoaded != null) {
                    loader.logDebug("import already loaded: " + namespace + ", skipping",
                                    "schemaLoader");
                    continue;
                }
            }
            if (importDef.location && importDef.location != loader.location) {
                var requested = loader.loadImport(namespace, importDef.location, function (loadedObj) {
                    if (isc.isA.WebService(loadedObj)) {
                        loader.addWebService(loadedObj, namespace);
                    } else {
                        loader.addSchemaSet(loadedObj, namespace);
                    }
                    loader._importCount--;
                    loader.logInfo(loader + " loaded import: " + loadedObj +
                                    " as namespace: " + namespace +
                                    ", remaining imports: " + loader._importCount,
                                    "schemaLoader");
                    if (loader._importCount == 0) loader.doneImporting();
                }, importDef.isWSDL);
                if (requested) loader._importCount++;
            }
        }
        // no attempts to load imports, fire callback now
        if (loader._importCount == 0) loader.doneImporting();
    },
    loadImport : function (namespace, location, callback, isWSDL, loader) {
        // "location" attribute is intended to the location of the file doing the import, so
        // combine URLs
        var baseDir = loader.location.substring(0, loader.location.lastIndexOf("/"));
        if (!baseDir.endsWith("/")) baseDir += "/";
        var url = isc.Page.combineURLs(baseDir, location);

        // strangely XML schema files sometimes import themselves
        if (url == loader.location) {
            loader.logDebug("skipping self-reference import: " + url, "schemaLoader");
            return false;
        }

        // skip certain pedantic imports like importing the XML namespace
        if (this._ignoreImports.contains(url)) {
            loader.logDebug("skipping pedantic import: " + url, "schemaLoader");
            return false;
        }

        // skip definitely redundant loads (this page has already loaded from this URL)
        if (this._allImports.contains(url)) {
            loader.logDebug("skipping redundant import: " + url, "schemaLoader");
            return false;
        }
        this._allImports.add(url);

        loader.logInfo("loading import from: " + url +
                       "\nschema/service base dir: " + baseDir +
                       "\nimport location: " + location, "schemaLoader");

        var method = isWSDL ? "loadWSDL" : "loadXMLSchema";
        isc.xml[method](url, function (schemaSet) {
            loader.fireCallback(callback, "schemaSet", [schemaSet]);
        }, null, true, {
            // track the initiator of any series of schema loads (first WebService / SchemaSet
            // to have loaded)
            initiator:loader.initiator || loader,
            // recursively capture XML if the original call asked for it
            captureXML:loader.captureXML
        });
        return true;
    },
    _ignoreImports : [
        "http://www.w3.org/2001/xml.xsd",
        "http://www.w3.org/2001/XMLSchema",
        "http://www.w3.org/XML/1998/namespace"
    ],
    _allImports : []
});

isc.SchemaSet.getPrototype().toString = function () {
    return "[" + this.Class + " ns=" + this.echoLeaf(this.schemaNamespace) +
        (this.location ? " location=" + isc.Page.getLastSegment(this.location) : "") + "]";
};

//> @class WSRequest
// A WSRequest (or "web service request") is an extended RPCRequest with additional properties
// applicable to WSDL/SOAP web services.
// <P>
// All properties which are legal on +link{class:RPCRequest} are legal on a WSRequest, in
// addition to the properties listed here.
//
// @treeLocation Client Reference/Data Binding
// @see RPCRequest
// @visibility external
//<

//> @attr wsRequest.wsOperation (String : null : IR)
// Name of the web service operation to invoke.
//
// @visibility external
//<

//> @attr wsRequest.data (any : null : IR)
// Data to be serialized to XML to form the SOAP body.
//
// @visibility external
//<

//> @attr wsRequest.useFlatFields (boolean : null : IR)
// @include dsRequest.useFlatFields
// @visibility external
//<

//> @attr wsRequest.xmlNamespaces (Object : null : IR)
// Optional object declaring namespace prefixes for use in evaluating the
// <code>resultType</code> parameter of +link{WebService.callOperation()}, if resultType is an
// XPath.
// <P>
// Format is identical to +link{operationBinding.xmlNamespaces}, and default namespaces
// bindings are also identical.
//
// @visibility external
//<

//> @attr wsRequest.xmlResult (boolean : false : IR)
// Valid only with +link{WebService.callOperation()}.  If set, do not transform XML results to
// JavaScript.  Instead just return the XML nodes selected by the passed XPath or recordName,
// or all nodes within the SOAP body if no XPath was passed.
//
// @visibility external
//<

//> @attr wsRequest.headerData (any : null : IR)
// Data to be serialized to form the SOAP headers, as a map from the header part name to the
// data.  For example, given WSDL like this:
// <pre>
//     &lt;soap:header part="SessionHeader" message="tns:HeaderMessage"/&gt;
//     &lt;soap:header part="CallOptions" message="tns:HeaderMessage/&gt;
// </pre>
// <code>headerData</code> like this might be provided:
// <pre>
//     dsRequest.headerData =
//         { SessionHeader : <i>data</i>
//           CallOptions : <i>data</i> };
// </pre>
// The provided data will be serialized to XML by the
// +link{webService.getInputHeaderSchema,SOAP header schema} via
// +link{dataSource.xmlSerialize()}
//
// @visibility external
//<



isc.defineClass("WebService").addMethods({
    //> @attr webService.serviceNamespace (URI : null : R)
    // Namespace of this WebService, derived from the <code>targetNamespace</code>
    // attribute of the <code>&lt;wsdl:definitions&gt;</code> element.
    //
    // @group webService
    // @visibility xmlBinding
    //<
    init : function () {
        // mark all messages with the service namespace
        var namespace = this.serviceNamespace;
        if (this.messages) {
            for (var i = 0; i < this.messages.length; i++) {
                this.messages[i].serviceNamespace = namespace;
            }
        }

        // register globally
        this.logInfo("registered service with serviceNamespace: " + namespace +
                     " service name: " + this.name);
        isc.WebService.services.add(this);

        // for loadWSDL() callback to return WebService
        isc.WebService._lastLoaded = this;
    },

    loadImports : function (callback) {
        isc.SchemaSet.loadImports(callback, this);
    },

    // override point for changing how imported schema are loaded
    // NOTE: duplicated in SchemaSet
    loadImport : function (namespace, location, callback, isWSDL) {
        return isc.SchemaSet.loadImport(namespace, location, callback, isWSDL, this);
    },
    doneImporting : function () {
        this.fireCallback(this._doneImportingCallback);
    },

    addSchemaSet : function (schemaSet, namespace) {
        this._schemaSets = this._schemaSets || [];
        this._schemaSets.add(schemaSet);
    },

    addWebService : function (webService, namespace) {
        this._webServices = this._webServices || [];
        this._webServices.add(webService);
    },

    // when captureXML has been set in loadWSDL / loadXMLSchema, this method is called on the
    // initiator of a series of schema loads so that complete source is available
    addImportXMLSource : function (xmlText, location) {
        this.importSources = this.importSources || [];
        this.importSources.add({
            xmlText : xmlText,
            location : location
        })
    },

    getOperation : function (operationName, portTypeName) {
        if (isc.isAn.Object(operationName)) return operationName;

        // ensure we've looked up related WSDL/XMLSchema imports
        if (!this._lookedUpImports) {
            isc.SchemaSet.findLoadedImports(this);
            this._lookedUpImports = true;
        }

        // look up the binding and portType definitions
        var bindingOperation = this.getBindingOperation(operationName, portTypeName);
        var portTypeOperation = this.getPortTypeOperation(operationName, portTypeName);

        if (!bindingOperation && !portTypeOperation) {
            this.logWarn(this + ": no such operation: '" + operationName + "'");
            return null;
        }

        // and combine into a structure that has everything we need to know
        return isc.addProperties({}, portTypeOperation, bindingOperation);
    },

    // find an operation from a list of bindings or portTypes
    findOperation : function (operationName, portTypeName, bindingList, isPortType) {
        if (!bindingList) return;

        // if portTypeName is specified, look in only <binding>s or <portType>s of that name
        if (portTypeName) bindingList = bindingList.findAll("portTypeName", portTypeName);
        if (!bindingList) return;

        // check imported web service definitions
        if (this._webServices) {
            for (var i = 0; i < this._webServices.length; i++) {
                var webService = this._webServices[i],
                    method = isPortType ? "getPortTypeOperation" : "getBindingOperation",
                    operation = webService[method](operationName, portTypeName);
                if (operation != null) return operation;
            }
        }

        // otherwise look in any of them, so that only operationName needs to be specified so
        // long as it's unique
        for (var i = 0; i < bindingList.length; i++) {
            var operations = bindingList[i].operation;
            if (!isc.isAn.Array(operations)) operations = [operations];
            var operation = operations.find("name", operationName);
            if (operation != null) return operation;
        }
    },

    // get an <operation> definition from a <portType>
    // portTypes contain the inputMessage and outputMessage
    getPortTypeOperation : function (operationName, portTypeName) {
        return this.findOperation(operationName, portTypeName, this.portTypes, true);
    },
    // get an <operation> definition from a <binding>
    // bindings contain the soapAction, parts (which parts of the message to use), soapEncoding
    // style, input and output namespace (for encodings that cause an element to be output
    // corresponding to the operation name)
    getBindingOperation : function (operationName, portTypeName) {
        return this.findOperation(operationName, portTypeName, this.bindings);
    },

    getOperationForMessage : function (messageName) {
        var operations = this.getOperations();
        if (!operations) return;
        var operation = operations.find("inputMessage", messageName);
        if (operation) return operation;
        operation = operations.find("outputMessage", messageName);
        if (operation) return operation;
    },

    //> @method webService.getOperationNames()
    // @return (Array) names of the available operations supported by this service (array of strings)
    // @group webService
    // @visibility xmlBinding
    //<
    getOperationNames : function () {
        // return cached list
        var operationNames = this.operationNames;
        if (operationNames) return operationNames;

        // ensure we've looked up related WSDL/XMLSchema imports to connect to separately
        // loaded portTypes
        if (!this._lookedUpImports) {
            isc.SchemaSet.findLoadedImports(this);
            this._lookedUpImports = true;
        }

        operationNames = this.operationNames = [];
        if (this.bindings) {
            for (var i = 0; i < this.bindings.length; i++) {
                var binding = this.bindings[i],
                    operations = binding.operation;
                if (!isc.isAn.Array(operations)) operations = [operations];
                operationNames.addList(operations.getProperty("name"));
                // find the corresponding portType operation and mark it as having a binding
                for (var j = 0; j < operationNames.length; j++) {
                    var ptOperation = this.getPortTypeOperation(operationNames[j], binding.portTypeName);
                    if (ptOperation) ptOperation.hasBinding = true;
                }
            }
        }
        // add all operations on portType that don't have a binding
        if (this.portTypes) {
            for (var i = 0; i < this.portTypes.length; i++) {
                var portType = this.portTypes[i],
                    operations = portType.operation;
                if (!isc.isAn.Array(operations)) operations = [operations];
                var unbound = operations.findAll("hasBinding", true);
                if (unbound) {
                    operations = operations.duplicate();
                    operations.removeAll(unbound);
                }
                operationNames.addList(operations.getProperty("name"));
            }
        }

        // expensive, so cache it
        return (this.operationNames = operationNames);
    },

    // get operation definitions for all of the operations supported by this web service
    getOperations : function (boundOnly) {
        var operationNames = this.getOperationNames(),
            operations = [];
        for (var i = 0; i < operationNames.length; i++) {
            var operation = this.getOperation(operationNames[i]);
            if (boundOnly && !operation.hasBinding) continue;
            operations.add(operation);
        }
        return operations;
    },

    //> @method webService.getSchema()
    // Get the schema definition of any complexType or element of complexType defined in any
    // &lt;schema&gt; blocks in the WSDL file this WebService represents.
    //
    // @param schemaName (String) name of type or element
    // @param [schemaType] (String) optional type of schema to return, either "element" for
    //                              xs:element definitions only or "type" for xs:complexType
    //                              definitions.  If unspecified, either will be returned,
    //                              with types preferred if names collide
    // @return (DataSource) requested schema
    // @group webService
    // @visibility xmlBinding
    //<
    getSchema : function (name, schemaType) {
        // look up all the schemaSets that the WSDL file referred to.
        // do this lazily so order of creation doesn't matter for SchemaSets and WebServices
        // loaded from one WSDL file
        if (!this._lookedUpImports) {
            isc.SchemaSet.findLoadedImports(this);
            this._lookedUpImports = true;
        }

        var schemaSets = this._schemaSets;
        if (schemaSets != null) {
            // look through each schemaSet for a schema of this name
            for (var i = 0; i < schemaSets.length; i++) {
                var schemaSet = schemaSets[i];
                var schema = schemaSet.getSchema(name, schemaType);
                if (schema) return schema;
            }
        }

        // finally, look globally.  This is key for discovering schema loaded from separate
        // files via separate calls to loadXMLSchema.
        return isc.DS.get(name, null, null, schemaType);
    },

    // get the request or response message schema
    getRequestMessage : function (operationName) {
        var operation = this.getOperation(operationName);
        return this.getMessage(operation.inputMessage);
    },
    getResponseMessage : function (operationName) {
        var operation = this.getOperation(operationName);
        return this.getMessage(operation.outputMessage);
    },

    getMessage : function (messageName) {
        var message = this.messages.find("ID", "message:" + messageName);
        if (message) return message;

        // ensure we're connected to any imported WSDL services, which may contain message
        // definitions
        if (!this._lookedUpImports) {
            isc.SchemaSet.findLoadedImports(this);
            this._lookedUpImports = true;
        }

        // look in imported services
        if (this._webServices) {
            for (var i = 0; i < this._webServices.length; i++) {
                var webService = this._webServices[i];
                message = webService.getMessage(messageName);
                if (message) return message;
            }
        }
    },

    getBodyPartNames : function (operationName, isOutput) {
        var operation = this.getOperation(operationName),
            bodyParts = isOutput ? operation.outputParts : operation.inputParts;
        if (bodyParts == null || isc.isAn.emptyString(bodyParts)) {
            // all body parts should be used
            var message = isOutput ? this.getResponseMessage(operationName) :
                                     this.getRequestMessage(operationName);
            return message.getFieldNames();
        } else {
            return bodyParts.split(" ");
        }
    },

    //> @attr webService.globalNamespaces (Object : ... : IRW)
    // @include dataSource.globalNamespaces
    //<
    globalNamespaces : {
        xsi: "http://www.w3.org/2001/XMLSchema-instance",
        xsd: "http://www.w3.org/2001/XMLSchema"
    },

    //> @method webService.callOperation()
    // Invoke a web service operation.
    // <P>
    // The <code>data</code> parameter will be serialized to XML to form the input message for
    // the operation, as described by +link{method:DataSource.xmlSerialize()}.  Namespacing,
    // element ordering, and SOAP encoding rules are automatically followed.  If the web
    // service you are trying to contact requires a complicated nested structure, consider
    // using +link{wsRequest.useFlatFields} to simplify the required JavaScript input data.
    // <P>
    // The <code>resultType</code> selects what part of the message should be decoded to
    // JavaScript and made available as the "data" variable in the callback.  The
    // <code>resultType</code> parameter can be either:
    // <ul>
    // <li> an XPath.  "data" will be always be an Array, containing the selected elements as
    // decoded by +link{XMLTools.toJS()}.  All properties will have String value.
    // <li> the name of an XML Schema type found somewhere in the response.  You can use the
    // WSDL tab of the Developer Console to analyze the WSDL file for an appropriate type name.
    // "data" will be an Array, containing the decoded elements as decoded by
    // +link{dataSource.recordsFromXML()}.  In this case, since the XML Schema type of the
    // selected data is known, properties will have correct type (eg "date" fields will
    // have JavaScript Date objects)
    // <li> null.  "data" will an Object representing the entire &lt;SOAP:Body&gt; as decoded
    // to JavaScript.  As above, properties will have correct type.
    // </ul>
    // In the callback, you also receive the XML document returned by the web service as
    // "xmlDoc".
    // <P>
    // NOTE: <code>callOperation()</code> is appropriate for simple operations that do not
    // involve DataBound Components, such as logging into a web service, or retrieving simple
    // String data.  <code>callOperation()</code> can also be used to retrieve small, read-only
    // datasets such as the option list for a SelectItem, but only if the dataset is guaranteed
    // to remain small enough for paging to be unnecessary.  For any larger datasets or
    // anything that will be edited, DataSource integration is more appropriate.
    //
    // @param operationName (String)    Name of the operation to invoke
    // @param data          (Object)    data to serialize as XML to form the inbound message of
    //                                  the operation
    // @param resultType    (Type or ElementName or XPath) Type, Element name, or XPath that
    //                                  should be selected from the result.  For XPaths, see
    //                                  +link{wsRequest.xmlNamespaces} for available namespace
    //                                  prefixes and how to add more.
    // @param callback      (Callback)  Callback to invoke on completion.  Signature
    //                                  callback(data, xmlDoc, rpcResponse, wsRequest)
    // @param requestProperties (WSRequest Properties) Additional properties for the WSRequest, such
    //                                      as HTTPHeaders
    //
    // @group webService
    // @visibility xmlBinding
    // @example wsdlOperation
    //<

    callOperation : function (operationName, data, resultType, callback, requestProperties)
    {

        var operation = this.getOperation(operationName);
        if (operation == null) {
            this.logWarn("No such operation: " + operationName);
            return;
        }

        requestProperties = requestProperties || isc.emptyObject;

        var wsRequest = isc.addProperties({
            actionURL: this.getDataURL(operationName),
            httpMethod: "POST",
            contentType: "text/xml",
            data : data,
            serviceNamespace : this.serviceNamespace,
            // NOTE: this ensures that all DataSources involved in serialization consistently
            // lookup the WebService instance that callOperation was called on, even if there
            // are multiple WSDL files that defined <WebService>s in a common namespace
            serviceName : this.name,
            wsOperation : operationName
        }, requestProperties);

        wsRequest.httpHeaders = isc.addProperties({},
                                                  { SOAPAction : operation.soapAction || '""' },
                                                  requestProperties.httpHeaders);

        wsRequest.headerData = requestProperties.headerData || this.getHeaderData(wsRequest);

        // create the SOAP message based on the WSRequest
        wsRequest.data = this.getSoapMessage(wsRequest);


        wsRequest.internalClientContext = {
            _callOperationCallback : callback,
            _operationName : operationName,
            _resultType : resultType,
            // special flag to return selected XML nodes without JS translation
            _xmlResult : requestProperties.xmlResult
        };

        if (this.spoofResponses) {
            var sampleResponse = this.getSampleResponse(operationName);
            if (this.logIsDebugEnabled("xmlBinding")) {
                this.logDebug("spoofed response:\n" + sampleResponse, "xmlBinding");
            }
            this.delayCall("_callOperationReply",
                           [isc.xml.parseXML(sampleResponse), sampleResponse,
                            {status:0,
                             internalClientContext: wsRequest.internalClientContext,
                             httpResponseCode:200,
                             httpResponseText:sampleResponse}, wsRequest]);
            return;
        }

        wsRequest.callback = { target:this, methodName:"_callOperationReply" };
        isc.xml.getXMLResponse(wsRequest);
    },
    _callOperationReply : function (xmlDoc, xmlText, rpcResponse, rpcRequest) {
        var context = rpcRequest.internalClientContext,
            operationName = context._operationName,
            resultType = context._resultType;

        // If there was an error, we will only arrive here if willHandleError is true.
        // Just fire the user's callback and let them decide what to do.
        if (rpcResponse.status < 0) {
            this.fireCallback(context._callOperationCallback,
                              "data,xmlDoc,rpcResponse,wsRequest",
                              [rpcResponse.data,xmlDoc,rpcResponse,rpcRequest]);
            return;
        }

        xmlDoc.addNamespaces(this.getOutputNamespaces(operationName));
        if (rpcRequest.xmlNamespaces) {
            xmlDoc.addNamespaces(rpcRequest.xmlNamespaces);
        }

        // we were passed a type (FIXME crude detection)
        var passedXPath = (resultType != null && resultType.contains("/")),
            xPath = (passedXPath ? resultType : null),
            data;

        if (passedXPath) {
            // apply XPath selector if passed one or passed resultType
            data = xmlDoc.selectNodes(xPath);
        } else if (resultType) {
            data = this.selectByType(xmlDoc, operationName, resultType);
        } else {
            // if no XPath or resultType was given, select the soap body
            data = xmlDoc.selectNodes("//s:Body/*",
                                      { s:"http://schemas.xmlsoap.org/soap/envelope/" });
            // don't create a spurious Array for the most common case of a singular body
            // element
            if (data.length == 1) data = data[0];
        }

        if (this.logIsDebugEnabled()) {
            this.logDebug("selected response data is: " + this.echoFull(data));
        }

        if (context._xmlResult) {
            // just return the raw XML nodes
            this.fireCallback(context._callOperationCallback,
                              "data,xmlDoc,rpcResponse,wsRequest",
                              [data,xmlDoc,rpcResponse,rpcRequest]);
            return;
        }

        // transform to JS
        var schema;
        if (passedXPath) {
            // if an xpath was passed, we don't know the schema of the selected elements, just
            // use schemaless transform
            schema = null;
        } else if (resultType) {
            // if we were passed a resultType, use that as the schema to transform nodes with
            // correct typing
            schema = this.getSchema(context._resultType);
        } else {
            // passed neither an xPath nor a resultType, so we selected the whole SOAP body.
            // We can use the message schema to decode the entire SOAP body, with correct
            // typing.
            var messageSchema =
                    this.getSchema("message:"+this.getOperation(operationName).outputMessage);

            if (this.getSoapStyle(operationName) != "document") {
                schema = messageSchema;
            } else {
                var firstField = messageSchema.getFieldNames().first();
                schema = messageSchema.getSchema(messageSchema.getField(firstField).type);
            }
        }
        //this.logWarn("transforming reply for operation: " + operationName +
        //             " toJS using schema " + schema);
        data = isc.xml.toJS(data, null, schema);

        this.fireCallback(context._callOperationCallback,
                          "data,xmlDoc,rpcResponse,wsRequest",
                          [data,xmlDoc,rpcResponse,rpcRequest]);
    },

    // when applying an XPath selector to the output of a web service, our default namespacing
    // strategy of providing all the namespaces declared on the document element
    // generally fails because we just get SOAP-related namespaces.  Furthermore, the web
    // service may use auto-generated prefixes for namespaces, so in general we can't rely on
    // the returned document alone for reasonable namespace prefixes.  Instead, provide the
    // schema namespace from the outermost element, and the service namespace
    getOutputNamespaces : function (operation, namespaces) {
        var schema = this.getDefaultOutputDS(operation);
        return isc.addProperties({
            "default" : schema.schemaNamespace || this.serviceNamespace,
            schema : schema.schemaNamespace,
            service : this.serviceNamespace
        }, namespaces);
    },

    getDataURL : function (operationName) {
        // NOTE: per-operation URLs can't be defined in WSDL, this is here for spoofing
        var operation = this.getOperation(operationName);
        if (operation && operation.dataURL) return operation.dataURL;
        return this.dataURL;
    },

    // SOAP message serialization
    // ---------------------------------------------------------------------------------------

    //> @method webService.getMessageSerializer() [A]
    // Get the schema used to serialize the entire request
    //
    // @param operationName (String or WSRequest Properties) name of the web service operation,
    //                                            or a WSRequest specifying it
    // @param forResponse (boolean) whether a serializer is request for the response message,
    //                              as opposed to the request message (the default)
    // @return (DataSource) schema used for serialization
    //<
    getMessageSerializer : function (operationName, forResponse) {
        var serializer = forResponse ? this.getResponseMessage(operationName)
                                     : this.getRequestMessage(operationName);

        if (serializer == null) {
            this.logWarn("no " + (forResponse ? "response" : "request") +
                         " message definition found for operation: '" + operationName + "'");
            return;
        }

        // in rpc-style soap, the outermost element of the body is named after the message
        // name.  In document-style soap, there is no element that corresponds to the message
        // name, only it's contents.  Therefore for document-style SOAP if there is exactly one
        // subelement of the message (the most common style by far), use that as the input
        // schema.  This means that when a message is supposed to look like this:
        //    <login>
        //       <username>bob</username>
        //       <password>mebob</password>
        //    </login>
        // The JS data you need to pass is:
        //    { username:"bob", password:"mebob" }
        // .. instead of the surprising and less obvious:
        //    { login : { username:"bob", password:"mebob" } }
        if (this.getSoapStyle(operationName) != "document") return serializer;

        var fieldNames = serializer.getFieldNames();
        if (fieldNames.length == 1 && serializer.fieldIsComplexType(fieldNames[0])) {
            var field = serializer.getField(fieldNames[0]);
            //this.logWarn("skipping message element and using field: " + this.echo(field));
            serializer = serializer.getSchema(field.type,
                                              field.xsElementRef ? "element" : null);
            if (serializer == null) {
                this.logWarn("can't find schema: " + field.type + ", part of " +
                             (forResponse ? "response" : "request") +
                             " message for operation '" + operationName + "'");
            }
        }
        return serializer;
    },

    // whether this operation uses simplified inputs, that is, does not expect data to contain
    // an object named after the message name, since the message name does not appear in the
    // generated message itself.  Useful for callers who form a data structure that exactly
    // corresponds to the message structure (ServiceOperation).
    useSimplifiedInputs : function (operationName, forResponse) {
        var normalSerializer = forResponse ? this.getResponseMessage(operationName)
                                           : this.getRequestMessage(operationName);
        return this.getMessageSerializer(operationName, forResponse) != normalSerializer;
    },

    //> @method webService.getSoapMessage() [A]
    // Return the SOAP message that will be formed from this WSRequest.
    //
    // @param wsRequest (WSRequest Properties) web service request object
    // @return (String) SOAP message
    // @visibility xmlBinding
    //<
    getSoapMessage : function (wsRequest, flags) {
        wsRequest.serviceNamespace = wsRequest.serviceNamespace || this.serviceNamespace;
        var operationName = wsRequest.wsOperation;

        if (this.getOperation(operationName) == null) {
            this.logWarn("no such operation: '" + operationName +
                         "' in service: " + this.serviceNamespace);
            return "";
        }

        var messageSerializer =
                this.getMessageSerializer(wsRequest.wsOperation,
                                          flags && flags.generateResponse);

        // already warned about in getMessageSerializer
        if (messageSerializer == null) return "";

        return messageSerializer.getXMLRequestBody(wsRequest, flags);
    },

    getSampleResponse : function (operationName, data, flags, returnRequest) {
        return this.getSoapMessage({
            wsOperation : operationName,
            data : data || {}
        }, isc.addProperties({
            spoofData:true,
            generateResponse:!returnRequest
        }, flags));
    },

    getSampleRequest : function (operationName, data, flags) {
        return this.getSampleResponse(operationName, data, flags, true);
    },

    // get the soap style, "document" or "rpc", which can be specified per operation or for the
    // service as a whole
    getSoapStyle : function (operationName) {
        return this.getOperation(operationName).soapStyle || this.soapStyle;
    },

    // ---------------------------------------------------------------------------------------




    //> @method webService.getInputDS()
    // Get a DataSource representing the input message to a web service operation.
    // <P>
    // This DataSource is suitable for use as
    // +link{DataBoundComponent.dataSource,form.dataSource} for a form that the user fills out
    // when providing inputs to call this web service operation.
    //
    // @param operationName (String) name of the web service operation whose inputs the
    //                               returned DataSource will represent
    // @return (DataSource) DataSource representing the input message of a web service
    //                      operation
    // @visibility xmlBinding
    // @example wsdlBinding
    //<
    getInputDS : function (operationName) {

        return this.getMessageSerializer(operationName);
    },

    getHeaderSchema : function (operationName, isInput) {
        var operation = this.getOperation(operationName),
            headers = isInput ? operation.inputHeaders : operation.outputHeaders;

        if (!headers) return null;

        var headerSchema = {};
        for (var i = 0; i < headers.length; i++) {
            var partName = headers[i].part,
                messageSchema = this.getSchema("message:"+headers[i].message);

            //this.logWarn("messageSchema: " + messageSchema);

            var partField = messageSchema.getPartField(partName);

            //this.logWarn("partField: " + this.echo(partField));
            // NOTE: simple type headers are legal, in which case we just return the field
            // definition
            headerSchema[partName] = this.getSchema(partField.type) || partField;
        }
        return headerSchema;
    },

    //> @method webService.getInputHeaderSchema()
    // Get the schema for each part of the SOAP header for the input message of a given
    // operation, as a mapping from part name to schema.  For example, given WSDL like:
    // <pre>
    //     &lt;soap:header part="SessionHeader" message="tns:HeaderMessage"/&gt;
    //     &lt;soap:header part="CallOptions" message="tns:HeaderMessage/&gt;
    // </pre>
    // The following schema would be returned:
    // <pre>
    //     { SessionHeader : <i>sessionHeaderPartSchema</i>,
    //       CallOptions : <i>callOptionsPartSchema</i> }
    // </pre>
    // The schema are instances of +link{DataSource} that can be inspected to discover the
    // elements and types that are legal in that header part, and can construct a valid SOAP
    // header part if +link{dataSource.xmlSerialize()} is invoked.
    //
    // @param operationName (String) name of an operation from this web service
    // @return (Object) mapping from partName to schema
    // @visibility xmlBinding
    //<
    getInputHeaderSchema : function (operationName) {
        return this.getHeaderSchema(operationName, true);
    },

    //> @method webService.getOutputHeaderSchema()
    // Get the schema for each part of the SOAP header for the output message of a given
    // operation, as a mapping from part name to schema.  For example, given WSDL like:
    // <pre>
    //     &lt;soap:header part="SessionHeader"/&gt;
    //     &lt;soap:header part="CallOptions"/&gt;
    // </pre>
    // The following schema would be returned:
    // <pre>
    //     { SessionHeader : <i>sessionHeaderPartSchema</i>,
    //       CallOptions : <i>callOptionsPartSchema</i> }
    // </pre>
    // The schema are instances of +link{DataSource} that can be inspected to discover the
    // elements and types that are legal in that header part, and can construct a valid SOAP
    // header part if +link{dataSource.xmlSerialize()} is invoked.
    //
    // @param operationName (String) name of an operation from this web service
    // @return (Object) mapping from partName to schema
    // @visibility xmlBinding
    //<
    getOutputHeaderSchema : function (operationName) {
        return this.getHeaderSchema(operationName, false);
    },

    //> @method webService.getHeaderData()
    // Override this method to return data that should be serialized as SOAP headers for the
    // current operation, such as a sessionId.
    // <P>
    // Format of the returned data is the same as that documented for
    // +link{dsRequest.headerData}.
    // <P>
    // The object passed to this method will be a true DSRequest in the case of a DataSource
    // operation, or just an Object with a "data" property for web service operations
    // initiated by +link{webService.callOperation}.
    // <P>
    // If <code>headerData</code> is instead provided via either dsRequest.headerData or as
    // part of the <code>requestProperties</code> parameter to
    // +link{webService.callOperation,callOperation()}, this method will never be called.
    //
    // @param dsRequest (DSRequest)
    // @return (Object) data for SOAP headers
    //
    // @visibility xmlBinding
    //<
    getHeaderData : function (dsRequest) { },

    // create an XPath selector that will select objects of the targetSchema from the output
    // message of the specified web service operation.
    // This is needed when we are interested in records of type "myObject", but which actually
    // have the tagName "records" in the result
    selectByType : function (xmlResponse, operationName, schemaName) {
        var operation = this.getOperation(operationName),
            outputMessage = this.getSchema("message:" + operation.outputMessage),
            targetSchema = this.getSchema(schemaName);

        if (targetSchema == null) {
            this.logWarn("selectByType: type '" + schemaName +
                         "' not present in schema for message: " + operation.outputMessage);
            return null;
        }

        // find the tagName the target schema will appear as in the response message
        var tagLocation = outputMessage.findTagOfType(targetSchema.ID);

        if (tagLocation == null) {
            this.logWarn("selectByType: no tag of type '" + schemaName +
                         "' could be found in message: " + operation.outputMessage);
            return null;
        }

        var tagLocationDS = tagLocation[0],
            tagName = tagLocation[1],
            parentSchema = tagLocation[2],
            parentSchemaTagName = tagLocation[3],
            field = tagLocationDS.getField(tagName);

        // if we couldn't find the tagName, use the type name as a fallback (this may indicate
        // a response message which is not completely specified in schema, eg xsd:any)
        tagName = tagName || targetSchema.ID;

        // element definitions that were top-level in the WSDL file have a schemaNamespace
        // attribute and must be namespaced within the response message.  Non-top-level element
        // definitions must not be, unless the <schema> element declares
        // elementFormDefault="qualified", in which case everything must be qualified.

        var qualify = targetSchema.mustQualify,
            namespace = targetSchema.schemaNamespace,
            xpath = "//" + (qualify ? "ns0:" : "") + tagName;

        /*

        if (parentSchema && !isc.isA.WSDLMessage(parentSchema) &&
            targetSchema.getFieldNames().length == 1)
        {
            qualify = parentSchema.mustQualify;
            namespace = parentSchema.schemaNamespace;
            xpath = "//" + (qualify ? "ns0:" : "") + parentSchemaTagName + "/*";
            this.logWarn("targetting parentSchema: " + parentSchema +
                         " fieldName " + parentSchemaTagName +
                         " namespace: " + namespace);
        }
        */

        // handle SOAP Array encoding, which specifies essentially that there is a container
        // tag whose children are of a specified type, which we represent as field.multiple
        if (field && field.multiple) xpath = xpath + "/*";

        var elements = isc.xml.selectNodes(xmlResponse, xpath, { ns0 : namespace });

        if (this.logIsDebugEnabled("xmlBinding")) {
            this.logDebug("selecting type: '" + targetSchema +
                          "' within message '" + operation.outputMessage +
                          " via XPath: " + xpath +
                          (qualify ? " using ns0: " + targetSchema.schemaNamespace : "") +
                          " got " + elements.length + " elements", "xmlBinding");
        }
        return elements;
    },

    // find the schema best suited for binding a grid or editor form to the results of a
    // web service operation.  Note this getInputDS() gives you the schema best suited for eg a
    // SearchForm.
    getDefaultOutputDS : function (operationName) {
        var schema = this.getResponseMessage(operationName);

        if (!schema) return null;
        // skip one level of pointless containment: a complexType with just one subelement,
        // which is also a complexType.
        var fieldNames = schema.getFieldNames();
        if (fieldNames.length == 1 && schema.fieldIsComplexType(fieldNames[0])) {
            return schema.getSchema(schema.getField(fieldNames[0]).type);
        }

        // improvements: find the first Array-like structure of elements containing simple type
        // fields.
        return schema;
    },

    //> @method webService.getFetchDS()
    // Retrieve a DataSource that provides read-only access to records returned by a web
    // service operation.
    // <P>
    // +link{interface:DataBoundComponent,DataBound Components} can be bound to the returned
    // DataSource, and the +link{ListGrid.fetchData(),fetchData()} method can be invoked
    // to retrieve data from the web service.
    // <P>
    // The returned DataSource is only capable of the "fetch"
    // +link{group:dataSourceOperations,DataSource operation}, not "update", "add" or
    // "remove".  To create a DataSource capable of full read-write access, use
    // +link{DataSource.operationBindings} with the
    // +link{OperationBinding.wsOperation,wsOperation} property set to associate each
    // DataSource operation with a web service operation.
    //
    // @param operationName (String) name of the web service operation to invoke to fetch
    //                               records
    // @param resultType    (String) tag or type name of the XML element to be returned as
    //                               DataSource records
    // @param [operationBindingProperties] (OperationBinding Properties)
    // Optional additional properties for the operationType:"fetch"
    // +link{OperationBinding,operationBinding} which this method automatically creates.  This
    // can be used to set properties such as +link{operationBinding.useFlatFields} or
    // +link{operationBinding.recordXPath}
    //
    // @group webService
    // @visibility xmlBinding
    //<
    getFetchDS : function (operationName, resultType, operationBindingProperties) {

        // if no resultType is specified, pick the first non-trivial structure
        if (resultType == null) resultType = this.getDefaultOutputDS(operationName);
        resultType = isc.isA.Object(resultType) ? resultType.ID : resultType;

        if (resultType != null && this.getSchema(resultType) == null) {
            this.logWarn("getFetchDS: resultType: '" + resultType +
                         "' not present in web service - missing XML files?");
        }

        // we subclass because we need operation-specific properties on this DataSource,
        // where it may be shared as the inputs or part of the inputs for another operation
        var fetchDS = isc.DS.create({
            // critical so this DS can find this WebService
            serviceNamespace : this.serviceNamespace,

            inheritsFrom : resultType,

            operationBindings : [
                isc.addProperties({
                    operationType: "fetch",
                    wsOperation:operationName,
                    recordName:resultType
                }, operationBindingProperties)
            ]
        });

        return fetchDS;
    },

    //> @method webService.setLocation() [A]
    // Set location can be used when the actual URL where a service will be accessible isn't
    // known until runtime, or changes at runtime, hence can't be embedded in the service
    // definition.
    // <P>
    // With an operation parameter, <code>setLocation()</code> can be used to set a distinct
    // URL for each web service operation.  This is a development-time only feature that allows
    // XML flat files to be placed at various URLs on a server, to serve as spoofed responses
    // for each web service operation.
    //
    // @param location (URL) URL where web service can be contacted
    // @param [operation] (String) optional operation name to set the location for, for
    //                             debugging only
    // @group webService
    // @visibility xmlBinding
    //<
    setLocation : function (location, operation) {
        if (operation) this.getBindingOperation(operation).dataURL = location;
        else this.dataURL = location;
    }
});

isc.WebService.addClassMethods({
    // NOTE: we create one WebService per .wsdl file, however, two <wsdl:definition>s can
    // appear in two different files with different <wsdl:service> elements.  In this case
    // the different <wsdl:service>s can be distinguished by the @name attribute on the
    // <wsdl:service>.
    services : [],

    //> @classMethod WebService.get()
    // Retrieve a WebService object by the targetNamespace declared on the &lt;wsdl:definitions&gt;
    // element in the WSDL file from which the WebService was derived.
    // <P>
    // If you have more than one &lt;wsdl:service&gt; in the same target namespace, use
    // +link{classMethod:WebService.getByName} to disambiguate.
    //
    // @param serviceNamespace (String) uri from the "targetNamespace" attribute of the
    // &lt;wsdl:definitions&gt; element in the WSDL file
    // @return (WebService) the requested WebService, or null if not loaded
    //
    // @group webService
    // @visibility xmlBinding
    // @example wsdlBinding
    //<
    get : function (serviceNamespace) {
        return this.services.find("serviceNamespace", serviceNamespace);
    },

    //> @classMethod WebService.getByName()
    // Retrieve a WebService object by the name attribute declared on the &lt;wsdl:service&gt; tag.
    //
    // @param serviceName (String) name attribute from the &lt;wsdl:service&gt; tag
    // @param [serviceNamespace] (String) optional serviceNamespace if needed to disambiguate
    // @return (WebService) the requested WebService, or null if not loaded
    //
    // @group webService
    // @visibility xmlBinding
    //<
    getByName : function (serviceName, serviceNamespace) {
        if (serviceName == "") serviceName = null;
        if (serviceNamespace != null) {
            return this.services.find({name: serviceName, serviceNamespace: serviceNamespace});
        } else {
            return this.services.find("name", serviceName);
        }
    }
});

isc.WebService.getPrototype().toString = function () {
    return "[" + this.Class + " ns=" + this.echoLeaf(this.serviceNamespace) +
        (this.location ? " location=" + isc.Page.getLastSegment(this.location) : "") + "]";
};

//> @groupDef wsdlBinding
// SmartClient supports automated integration with WSDL-described web services.  This support
// consists of:
// <ul>
// <li> creation of SOAP XML messages from JavaScript application data, with automatic
// namespacing, and support for both "literal" and "encoded" SOAP messaging, and "document" and
// "rpc" WSDL-SOAP bindings
// <li> automatic decode of SOAP XML messages to JavaScript objects, with strong typing (eg an
// XML schema "date" type becomes a JavaScript Date object)
// <li> +link{XMLTools.loadXMLSchema,import of XML Schema} (contained in WSDL, or external),
// including translating XML Schema "restrictions" to ISC +link{Validator,Validators}
// </ul>
// <P>
// WSDL services can be contacted by using +link{XMLTools.loadWSDL()} or the
// +link{group:loadWSDLTag,&lt;isc:loadWSDL&gt; JSP tag} to load the service definition, then
// invoking methods on the resulting +link{WebService} object.
// <P>
// +link{WebService.callOperation()} can be used to manually invoke operations for
// custom processing (example using +explorerExample{wsdlOperation,public zipcode service},
// examples using .NET at
// +externalLink{/examples/databinding/dotNET/temperatureConvert.jsp,/examples/databinding/dotNET/temperatureConvert.jsp}).
// <P>
// <b>Fetch-only DataSource binding</b>
// <P>
// To bind a component to a web service operation, call
// <P>
// &nbsp;&nbsp;+link{WebService.getFetchDS(),WebService.getFetchDS(<i>operationName,elementName</i>)}
// <P>
// to obtain a DataSource which describes the structure of an XML element or XML Schema type
// named <i>elementName</i>, which appears in the response message for the operation named
// <i>operationName</i>.  A component bound to this DataSource will show fields corresponding
// to the structure of the chosen XML element or type, that is, one field per subelement or
// attribute.  +link{ListGrid.fetchData(),fetchData()} called on this DataSource (or on a
// component bound to it) will invoke the specified web service operation, using the
// +link{Criteria} passed to fetchData() to fill out the input message via
// +link{dataSource.xmlSerialize()}, and using the specified XML element from the response
// message as data.
// <P>
// Similarly, +link{WebService.getInputDS,WebService.getInputDS(<i>operationName</i>)} returns
// a DataSource suitable for binding to a form that a user will fill out to provide inputs to
// the specified web service operation.  Typical use is to let the user fill in the form, then
// pass the results of +link{dynamicForm.getValues(),form.getValues()} to
// +link{listGrid.fetchData(),fetchData()} as criteria.
// <P>
// If the input message to the web service has extra nesting, consider using
// the +link{operationBinding.useFlatFields,useFlatFields} property to simplify the inputs
// required for <code>fetchData()</code>, and/or to simplify form databinding via
// +link{dataBoundComponent.useFlatFields,component.useFlatFields}.
// <P>
// Note that the WSDL tab in the Developer Console can provide a clean, simplified view of any
// WSDL file, making it easier to pick out the appropriate <code>operationName</code> and
// <code>elementName</code> parameters to pass to <code>getFetchDS()</code> and other
// +link{WebService} methods.
// <smartclient><P>
// Take a look at the +explorerExample{wsdlBinding,Google SOAP Search example} and the
// +externalLink{/examples/databinding/dotNET/customerSearch.jsp,.NET example}
// (/examples/databinding/dotNET/customerSearch.jsp).
// </smartclient>
// <P>
// <b>Binding with Customized Presentation</b>
// <P>
// Because XML Schema lacks key presentation metadata such as user-viewable titles, typically
// you cannot directly use the DataSources derived from XML Schema embedded in a WSDL file to
// drive visual component DataBinding in your final application.
// <P>
// You can create a DataSource that has custom fields <b>and</b> invokes a web
// service operation by setting +link{dataSource.serviceNamespace} to match the targetNamespace
// of the +link{WebService} (found on the <code>&lt;definitions&gt;</code> element from the
// WSDL file), and setting +link{operationBinding.wsOperation,wsOperation} to the name of the
// web service operation to invoke.  <code>fetchData()</code> called on such a DataSource will
// invoke the web service operation named by +link{operationBinding.wsOperation,wsOperation},
// just like a DataSource returned by +link{webService.getFetchDS()}.
// <P>
// In contrast to <code>getFetchDS()</code>, creating a DataSource in this way gives you the
// opportunity to:
// <ul>
// <li> declare arbitrary fields, with SmartClient presentation attributes such as titles and
// formatters
// <li> extract any data from the response message, via
// +link{operationBinding.recordXPath,operationBinding.recordXPath} and
// +link{dataSourceField.valueXPath,field.valueXPath}, and transform it with
// +link{dataSource.transformResponse,transformResponse()}
// <li> transform the inbound data, if necessary, in order to add metadata such as
// +link{dsRequest.startRow} for paging, or a sessionId for a service requiring authentication
// </ul>
// These techniques are shown in the +explorerExample{wsdlBinding,Google SOAP Search example}.
// <P>
// <b>XML Schema Reuse</b>
// <P>
// Having loaded a WSDL file, all of the XML Schema definitions within the service definition
// get translated to SmartClient +link{DataSource,DataSources} and
// +link{SimpleType,SimpleTypes} via the rules described by +link{XMLTools.loadXMLSchema()},
// and are available to you via +link{webService.getSchema()} and +link{dataSourceField.type}.
// <P>
// You can use the +link{dataSource.inheritsFrom} property to create DataSources that extend
// from XML schema definitions, then add presentation metadata not found in XML schema.
// <P>
// Even if you choose to declare all fields manually, you can leverage XML Schema
// &lt;simpleType&gt; definitions by setting +link{DataSourceField.type,field.type} to the name
// of an XML Schema simple type embedded in the WSDL file.
// <P>
// <b>Round Trip Binding [fetch -> edit -> save]</b>
// <P>
// For full read-write integration with a service that supports the basic
// +link{group:dataSourceOperations,DataSource operations} on persistent data,
// +link{OperationBinding,OperationBindings} can be declared for each DataSource operation, and
// the +link{operationBinding.wsOperation,wsOperation} property can be used to to bind each
// +link{group:dataSourceOperations,DataSource operation} (fetch, update, add, remove) to a
// corresponding web service operation.
// <P>
// For example, this code accomplishes part of the binding to the
// +externalLink{http://www.google.com/search?q=sforce+partner+wsdl,SalesForce partner web services}
// (additional code is required to handle authentication and other details):
// <pre>
// isc.DataSource.create({
//    serviceNamespace : "urn:partner.soap.sforce.com",
//    operationBindings : [
//        { operationType:"fetch", wsOperation:"query", recordName: "sObject" },
//        { operationType:"update", wsOperation:"update", recordName: "SaveResult" },
//        { operationType:"add", wsOperation:"create", recordName: "SaveResult" },
//        { operationType:"remove", wsOperation:"delete", recordName: "DeleteResult" }
//    ],
//    ...
// });
// </pre>
// NOTE: additional code is required to handle authentication and other details, see the
// complete code in smartclientSDK/examples/databinding/SalesForce.
// <P>
// In this usage, any DSRequest performed on this DataSource invokes the web service operation
// named by the <code>wsOperation</code> property on the corresponding operationBinding, and
// +link{dsRequest.data} is serialized via +link{dataSource.xmlSerialize()} to form the input
// message to send to the web service.  For example, if a +link{DynamicForm.saveData()} is
// invoked and triggers a DSRequest with operationType:"add", the DataSource above will invoke
// the "create" operation, and +link{DynamicForm.getValues(),form.values} will become
// +link{dsRequest.data} and be serialized to form the input message of the "create" web
// service operation.
// <P>
// Typical usage is:
// <ol>
// <li> declare a DataSource that represents the fields of the object as you want them
// represented in the UI.  This DataSource is considered the "entity DataSource".  It may
// extend from an XML Schema complex type via +link{dataSource.inheritsFrom}.
// <li> use +link{operationBinding,operationBindings} to configure the entity DataSource to
// call the appropriate web service operations for each DataSource operation, and extract
// results via
// +link{operationBinding.recordXPath,recordXPath}/+link{operationBinding.recordName,recordName}
// <li> bind components as follows:
// <ul>
// <li> bind +link{listGrid,grids} to the entity DataSource
// <li> bind +link{SearchForm,SearchForms} to the input message of the fetch operation
// (obtained via +link{WebService.getInputDS,webService.getInputDS("operationName")}.  This is
// done because search inputs are frequently unrelated to the structure of the objects being
// searched for
// <li> bind forms use for editing ("add" and "update" operations) to the entity DataSource
// </ul>
// <li> use
// +link{dataSource.transformRequest,transformRequest}/+link{dataSource.transformResponse,transformResponse},
// +link{operationBinding.useFlatFields} and +link{operationBinding.responseDataSchema} to
// handle inconsistencies between the WSDL operations and the data you want in the presentation
// layer.
// </ol>
// A complete example of binding to the SalesForce "partner" web service, including
// authentication via SOAP headers, saving data and cache sync, inline editing, validation
// error handling and data paging, can be found in [webroot]/examples/databinding/SalesForce.
// <P>
// This requires a SalesForce account.  SalesForce currently offers
// +externalLink{http://www.google.com/search?hl=en&q=salesforce+developer+account,free developer accounts}.
// Please note: this application deals with <b>live data</b> and if you using inline editing
// <b>it will save to SalesForce</b>.
// <P>
// <b>Deployment</b>
// <P>
// For best performance, using the +link{group:loadWSDLTag,&lt;isc:loadWSDL&gt; JSP tag}
// is recommended, as it automatically caches a translated form of the WSDL file.  If you are
// not using the SmartClient server, the WSDL tab in the Developer Console allows you
// to save a .js file representing a WebService object, which can then be loaded and cached
// like a normal JavaScript file.
// <P>
// <B>Creating New WSDL Services</B>
// <P>
// If you have no existing WSDL web service but would like to use web services for integration,
// you can implement the "SmartClientOperations" web service described by the
// ${isc.DocUtils.externalLink(isc.Page.getIsomorphicDir()+"system/schema/SmartClientOperations.wsdl","WSDL file")}
// included in the SDK.  This simple, 4 operation web service can support any number of
// DataSources.  In this case, you create your DataSources as client-side instances of
// +link{WSDataSource} (general client-side DataSource creation is described under
// +link{group:dataSourceDeclaration,Creating DataSources}).  To change the URL where ISC
// expects to find the SmartClientOperations web service, use +link{WebService.setLocation()}
// like so:<pre>
//      var service = isc.WebService.get("urn:operations.smartclient.com");
//      service.setLocation("myURL");
// </pre>
// <P>
// To implement a web service <b>starting from a WSDL file</b>:
// <ul>
// <li>In the .NET framework, you will use the Web Services Description Language Tool
// +externalLink{http://www.google.com/search?q=wsdl.exe,(wsdl.exe)} to generate C# stubs that
// you will add business logic to
// <li>In Java, +externalLink{http://ws.apache.org/axis/,Apache Axis} can be used to generate
// Java stubs for implementing a web service
// <li>In Perl, the +externalLink{http://soaplite.com,SOAP:Lite} module can be used to
// implement web services without code generation
// <li>for PHP, the NuSoap module can likewise be used to implement web services without code
// generation
// </ul>
//
// @visibility xmlBinding
// @treeLocation Client Reference/Data Binding
// @title WSDL Binding
//<





//>    @class RPCManager
//
// RPCManager is a static singleton class that manages transparent client/server RPC (remote
// procedure call).  This class provides a generic, low-level client/server communication
// integration point.
// <P>
// SmartClient's powerful databinding subsystem (see +link{DataSource},
// +link{DataBoundComponent,DataBoundComponents}) automatically make use of this class to issue
// RPCs as necessary, based on the
// +link{group:dataSourceOperations,DataSource protocol}. To integrate DataBoundComponents
// with your server, +link{group:clientServerIntegration,start here}.
// <P>
// For arbitrary client/server interactions outside of the DataSource subsystem, the
// SmartClient server also provides the +link{group:dmiOverview,Direct Method Invocation} feature.
// <P>
// The RPCManager class can also be used <i>directly</i> to send data to a URL of your
// choosing and optionally be called back with server-returned data when the server replies.
// <P>
// The SmartClient +link{group:iscServer,server code} has APIs for processing RPC requests
// providing features such as automatic Java &lt;--&gt; JavaScript object translation
// and handling of queued requests.<br>
// The +link{group:servletDetails,IDACall servlet} makes use of these features to handle standard
// +link{DataSource} requests and +link{DMI} calls. Developers can also override the
// <code>actionURL</code> of specific requests and use these APIs directly in a
// JSP, Servlet or Filter.
// <P>
// Note: the client-side RPCManager class can also be used without the SmartClient server.
// For an overview of client/server interactions without the SmartClient server, see
// +link{group:nonJavaBackend,this overview}.
// <P>
// <u>Simple arbitrary Remote Procedure Call example (client code):</u>
// <smartclient>
// <P>
// <pre>
// var data = { here: "is some data", to: ["send to the server"]};<br>
// RPCManager.sendRequest({ data: data, callback: "myCallback(data)", actionURL: "/rpcHandler.jsp"});<br>
// function myCallback(data) { alert("response from the server: " + data); }
// </pre>
// </smartclient>
// <smartgwt>
// <P>
// <pre>
//  RPCRequest request = new RPCRequest();
//  // Note data could be a String, Map or Record
//  request.setData("Some data to send to the client");
//  request.setActionURL("/rpcHandler.jsp");
//
//  RPCManager.sendRequest(request,
//      new RPCCallback () {
//          public void execute(RPCResponse response, Object rawData, RPCRequest request) {
//              SC.say("Response from the server:" + rawData);
//          }
//      }
//  );
// </pre>
// </smartgwt>
// <P>
// <u>Simple arbitrary Remote Procedure Call example (server code: /rpcHandler.jsp):</u>
// <br><br><pre>
// RPCManager rpc = new RPCManager(request, response, out);
// Object data = rpc.getData();
// System.out.println("client sent: " + data.toString());
// rpc.send("here's a response");
// </pre>
// <P>
// <u><b>Queuing</b></u>
// <br>
// Because of browser limitations on the total number of simultaneous HTTP connections to a given
// server, batching multiple RPC requests into a single HTTP request is highly advisable whenever
// possible.  The RPCManager provides a queuing mechanism that allows this.
// <br><br>
// <u>Queuing example (client code):</u>
// <smartclient>
// <pre>
// var wasQueuing = RPCManager.startQueue();
// RPCManager.send("a string of data", "myCallback(data)", {actionURL: "/rpcHandler.jsp"});
// RPCManager.sendRequest({ data: ["some", "more data", 2], callback: "myCallback(data)", actionURL: "/rpcHandler.jsp"});
// RPCManager.sendRequest({ data: "different callback", callback: "myCallback2(data)", actionURL: "/rpcHandler.jsp"});
// if (!wasQueuing) RPCManager.sendQueue();
//
// function myCallback(data) { alert("response from the server: " + data); }
// function myCallback2(data) { alert("response from the server (other callback): " + data); }
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
// boolean wasQueuing = RPCManager.startQueue();
//
// RPCCallback callback = new RPCCallback() {
//     public void execute(RPCResponse response, Object rawData, RPCRequest request) {
//         Window.alert("response from server:" + rawData);
//     }
// };
//
// RPCRequest request1 = new RPCRequest();
// request1.setActionURL("/rpcHandler.jsp");
// request1.setData("A String of Data");
// RPCManager.sendRequest(request1, callback);
//
// RPCRequest request2 = new RPCRequest();
// request2.setActionURL("/rpcHandler.jsp");
// request2.setData("Another String of Data");
// RPCManager.sendRequest(request2, callback);
//
// if (!wasQueuing) RPCManager.sendQueue();
// </pre>
// </smartgwt>
// <p>
// <u>Queuing example (server code: /rpcHandler.jsp):</u>
// <br><br><pre>
// RPCManager rpc = new RPCManager(request, response, out);
//
// for(Iterator i = rpc.getRequests().iterator(); i.hasNext();) {
//     RPCRequest rpcRequest = (RPCRequest)i.next();
//     Object data = rpcRequest.getData();
//     System.out.println("client sent:" + data.toString());
//
//     //send back the data sent to us by the client
//     rpc.send(rpcRequest, new RPCResponse(data));
// }<br>
// </pre>
// <br><br>
// <u><b>Error Handling</b></u><br><br>
// Please see this +link{group:errorHandling,separate article} on error handling.
// <br>
//
// @treeLocation Client Reference/RPC
// @visibility external
//<
isc.ClassFactory.defineClass("RPCManager");
isc.RPC = isc.rpc = isc.RPCManager;
//>Offline
isc.Page.observe(isc, "goOffline", "isc.rpc.goOffline()");
isc.Page.observe(isc, "goOnline", "isc.rpc.goOnline()");
//<Offline



// ---------------------------------------------------------------------------------------
//> @method Callbacks.LoadScreenCallback
// A +link{type:Callback} to evaluate when a screen is loaded via +link{RPCManager.loadScreen()}.
//
// @param [screen] (Canvas) The last top-level component loaded
// @param [rpcResponse] (RPCResponse)
// @param [suppressedGlobals] (Map) A collection of suppressed globals.
//
// @visibility external
//<

isc.ClassFactory.defineClass("LoadScreenCallback");
isc.LoadScreenCallback.addProperties({
    execute : function() {
    }
});

// ---------------------------------------------------------------------------------------
//>    @class RPCRequest
//
// Encapsulates a client/server RPC request.  You'll need to provide an instance of this class (or a
// constructor for it) to the +link{classMethod:RPCManager.sendRequest()} method.  If you use the
// +link{classMethod:RPCManager.send()} method, an instance of RPCRequest will be created for you.
//
// @see RPCManager.send()
// @see RPCManager.sendRequest()
// @visibility external
// @treeLocation Client Reference/RPC
//<
isc.ClassFactory.defineClass("RPCRequest");

isc.RPCRequest.addClassMethods({
    //> @classMethod RPCRequest.create()
    // RPCRequest shouldn't be created directly. Instead, pass +link{Properties} to
    // +link{RPCManager.sendRequest()} and +link{RPCManager.send()}.
    // @visibility external
    //<
    // Log a warning if called directly
    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {
        this.logWarn(
            "An RPCRequest does not need to be created. Instead, pass properties to methods " +
            "such as RPCManager.send() and RPCManger.sendRequest."
        );

        return isc.addProperties({}, A,B,C,D,E,F,G,H,I,J,K,L,M);
    }
});

//> @attr rpcRequest.data (String | Record | Object : null : IRW)
//
// This attribute specifies the payload of the RPCRequest.
// <smartclient>
// When using the +link{group:iscServer,SmartClient server},
// any JavaScript simple type or arbitrarily nested set of Objects and Arrays can be sent
// to server and automatically translated to Java Objects.
// </smartclient>
// <smartgwt>
// When using the +link{group:iscServer,SmartClient server}, objects sent to the server as
// <code>request.data</code> will be available on the server-side <code>RPCRequest</code>
// object as Java Objects. This is achieved by serializing the client side data
// in a JSON type format and generating Java Objects on the server from this serialized data.
// <P>
// If the client side <code>request.data</code> is set to a Java object in your SmartGWT code
// it will be serialized as JSON as follows:<br>
// - Numeric client side values (int, Integer, etc) will be serialized as JavaScript numbers.<br>
// - String values will be serialized as JavaScript strings.<br>
// - Date values will be serialized as JavaScript dates.<br>
// - Maps or Record objects will be serialized as JavaScript Objects.<br>
// - Arrays or Lists will become JavaScript arrays<br>
// Serialization of Maps and Arrays is recursive - each entry in an Array, or attribute
// on a Map will also be serialized according to the above rules.<br>
// Note that you can also set <code>request.data</code> directly to a JavaScriptObject,
// and use the <code>JSOHelper</code> class or <code><i>SomeObject.</i>getJSObj()</code> to
// perform your own data conversions on the client. The serialized JavaScript will then be
// converted back to Java on the server according to the following rules.
// </smartgwt>
// <P>
// Here are the
// mapping of JavaScript types to their corresponding server object types:<br><br>
//
// <table class='normal' border='1'>
//   <tr><td><b>JS Type</b></td>     <td><b>Java Type</b></td></tr>
//   <tr><td>Object: {}</td>         <td>Map</td></tr>
//   <tr><td>Array: []</td>          <td>List</td></tr>
//   <tr><td>String</td>             <td>String</td></tr>
//   <tr><td>Number</td>             <td>Long|Double</td></tr>
//   <tr><td>Boolean</td>            <td>Boolean</td></tr>
//   <tr><td>Date</td>               <td>java.util.Date</td></tr>
//   <tr><td>String</td>             <td>com.smartgwt.client.types.ValueEnum</td></tr>
//
// </table>
// <br><br>
// Note that the order of keys/values in the Maps created on the server is not guaranteed
// because JavaScript Object literals do not guarantee order.
// <p>
// Server->client conversion follows this table as well, with some extras.  See the toJS()
// method on JSTranslater in the server documentation for a description of additional
// behaviors.
// <P>
// When <b>not</b> communicating with the SmartClient server, <code>rpcRequest.data</code>
// becomes simple HTTP parameters or an HTTP request body - see +link{rpcRequest.useSimpleHttp}
// for details.
//
// @see RPCResponse.data
// @visibility external
//<

//> @attr rpcRequest.actionURL (URL : RPCManager.actionURL : IRW)
//
// Overrides RPCManager.actionURL for this request only.  If you're using queuing, note that queues
// as per-URL - in other words all RPCRequests in a queue must go to a single URL.  If you attempt
// to send a request with an actionURL that is different from those already in the queue, it
// will be sent to the server separately, ahead of the queue,  and a warning will be logged to
// the Developer Console.
//
// @see classAttr:RPCManager.actionURL
//
// @visibility external
//<

//> @attr rpcRequest.useHttpProxy (boolean : null : IR)
// Indicates whether this request should use the HttpProxyServlet in order to enable contacting
// hosts other than the origin server (available only in Pro Edition or better).
// <P>
// When various UI components issues requests automatically, or when a call to
// +link{RPCManager.sendProxied()} is made, the HttpProxy will automatically be used for a URL
// that starts with "http" and uses a hostname other than "localhost" or
// <code>window.location.hostname</code>, or if the port number differs.
// <P>
// <code>rpcRequest.useHttpProxy</code> should only be used to force requests to go through the
// HttpProxy when the above rules don't work, or to avoid using the HttpProxy when contacting
// hosts that allow cross-site calls via the
// +externalLink{http://www.google.com/search?q=http+access+control,Http Access Control}
// standard.
// <P>
// You can also set +link{RPCManager.useHttpProxy}:false to avoid ever using the
// HttpProxyServlet.
//
// @visibility external
//<

//> @groupDef rpcPrompt
// The properties in this group all deal with setting and styling a modal prompt during an RPC
// call to the server.
// @visibility external
//<

//> @attr rpcRequest.promptStyle (PromptStyle : RPCManager.promptStyle : IRW)
// Controls the prompt style for this request only.  Defaults to
// +link{RPCManager.promptStyle}.
//
// @see RPCManager.promptStyle
// @group rpcPrompt
// @visibility external
//<

//> @classAttr rpcRequest.useCursorTracker (boolean : platform-dependent : IRW)
//
// If true, an image is shown to the right of the cursor when +link{rpcRequest.promptStyle} is
// set to "cursor", otherwise the cursor itself is modified via css to the value of
// +link{rpcRequest.promptCursor}.  The default is platform-dependent.  In Safari, IE 5.5 and
// Firefox 1.0 the default is true, on all other platforms it is false.  The reason for this
// split is that the above browsers require that the cursor move before CSS settings are
// re-evaluated - this means the progress cursor can stick until the user moves the mouse.
// <p>
// If left unspecified, the default value is set by +link{RPCManager.useCursorTracker}.
//
// @see RPCManager.useCursorTracker
// @group rpcPrompt
// @visibility external
//<

//> @attr rpcRequest.promptCursor (String : browser-dependent : IRW)
// Controls the cursor shown when +link{rpcManager.promptStyle} is set to
// <code>"cursor"</code> for this request only. Defaults to +link{RPCManager.promptCursor}.
// <p>
// In Safari, IE 5.5 and Firefox 1.0 the default value is "wait", on all other platforms it is
// "progress".  The reason for this split is that the above-mentioned browsers do not support
// CSS2.1 - which is required for the "progress" cursor type.
//
// @see RPCManager.promptCursor
// @group rpcPrompt
// @visibility external
//<


//> @attr rpcRequest.prompt (HTMLString : RPCManager.defaultPrompt : IRW)
//
// Overrides RPCManager.defaultPrompt for this request only.  If you're using queuing, note that the
// prompt string from the first request in the queue is the one that is shown to the user.
//
// @see classAttr:RPCManager.defaultPrompt
// @see classAttr:RPCManager.showPrompt
// @see classAttr:RPCManager.promptStyle
// @see classAttr:RPCManager.promptCursor
// @see attr:rpcRequest.showPrompt
// @see attr:rpcRequest.promptStyle
// @see attr:rpcRequest.promptCursor
//
// @group rpcPrompt
// @visibility external
//<

//> @attr rpcRequest.showPrompt (Boolean : null : IRW)
// Overrides <code>RPCManager.showPrompt</code> for this request only.
// <p>
// If you're using queuing, note that if any of the requests in the queue specify
// showPrompt:true, then a prompt will be shown for the entire queue with the prompt text of
// the first request in the queue to specify a custom prompt if promptStyle is set to "dialog".
// <p>
// If promptStyle is set to "cursor" for the request that specified showPrompt: true, then the
// entire queue uses the "cursor" style for the prompt.
//
// @see classAttr:RPCManager.showPrompt
// @group rpcPrompt
// @visibility external
//<

//> @attr rpcRequest.promptDelay (number : RPCManager.promptDelay : IRWA)
//
// Overrides RPCManager.promptDelay for this request only. Defaults to +link{RPCManager.promptDelay}.
// <p>
// If you're using queuing, note that the promptDelay of the first request is used for the entire queue.
//
// @see attr:rpcRequest.showPrompt
// @see classAttr:RPCManager.promptDelay
// @group rpcPrompt
// @visibility external
//<

//> @attr rpcRequest.callback (RPCCallback : null : IRW)
//
// If you expect to receive a response to your RPC request, you can specify a callback that
// will be called with an instance or RPCResponse class as sent by the server.  Queuing does
// not affect callbacks in any way - your specified callback will be invoked for each
// RPCRequest that contained a callback regardless of whether the request was sent as part of a
// queue or not.
// <P>
// Note that if the request encounters an error (such as 500 server error), by default the
// callback will <b>not</b> be fired, instead, +link{RPCManager.handleError()} is called to
// invoke the default system-wide error handling.  Set +link{willHandleError}:true to have your
// callback invoked regardless of whether there are errors, however, make sure your callback
// properly handles malformed responses when +link{RPCResponse.status} is non-zero.  See the
// +link{group:errorHandling,error handling overview} for more details.
//
// @group errorHandling
// @visibility external
//<

//> @attr rpcRequest.clientContext (Object : null : IRW)
//
// An object to be held onto for the duration of the RPC turnaround to track
// application-specific context.
// <br>
// When an RPC turnaround completes, the <code>clientContext</code> is available in the
// +link{Callbacks.RPCCallback,RPCCallback} as <code>rpcResponse.clientContext</code>.  The
// <code>clientContext</code> is never sent to the server.
// <br>
// The <code>clientContext</code> is useful for holding onto state that will be used when the
// +link{Callbacks.RPCCallback,RPCCallback} fires, such as the name of a component that will receive the
// returned data.
//
// @see RPCResponse.clientContext
//
// @visibility external
//<

//> @attr rpcRequest.willHandleError (Boolean : false : IRW)
//
// With willHandleError:false, rpcResponses that indicate an error go through centralized
// handling in the RPCManager and rpcRequest.callback is never invoked.
// <P>
// Setting willHandleError:true means that your rpcRequest.callback will receive rpcResponses
// that have an error status and must handle them.
// <P>
// See also the error handling section in the +link{class:RPCManager} docs.
//
// @group errorHandling
// @see class:RPCManager
//
// @visibility external
//<

//> @attr rpcRequest.timeout (int : null : IRWA)
// Sets the timeout on this request.  Default is to use +link{RPCManager.defaultTimeout}.
// <p>
// If you're using +link{RPCManager.startQueue,queuing}, note that the timeout setting derived
// from the last request in the queue is used for the entire queue.  If you want to override
// the timeout for the queue, make sure to set your override at least on the last request in
// the queue.
// <p>
// For the "xmlHttpRequest" +link{rpcRequest.transport,transport}, this timeout can only happen
// if the server actually fails to respond within the specified number of milliseconds.  For
// the "hiddenFrame" transport, this timeout will occur for non-200 (HTTP_OK) responses.
// <p>
// If <code>timeout</code> is set to zero, the RPCManager will not enforce a timeout for this
// request.  However, note that all browsers enforce their own timeouts on HTTP requests, and
// may have different timeouts for different kinds of failures (no response at all from server,
// hung response after receiving headers, hung response after receiving partial data, etc).
// Also, intervening web proxies or firewalls may impose timeouts of their own.
// <p>
// As a rough rule of thumb, if your server response will have a lengthy pause before data
// begins to be sent, 1-2 minutes is the maximum allowable pause for a public site and still may
// not work for a minority of users, but up to 4 minutes may be allowable in a controlled
// environment (intranet or extranet with well-known user base).
// <p>
// Above these limits, your code should return some kind of immediate response to the browser,
// then kick off a server-side process to complete processing.  The browser can then either
// poll for completion, or use a server-push notification system such as SmartClient Real-Time
// Messaging (see +externalLink{http://smartclient.com/product}).
//
// @see classAttr:RPCManager.defaultTimeout
//
// @visibility external
//<

//> @attr rpcRequest.clientOnly (boolean : false : IRWA)
//
// Used for testing/prototyping without a server.  <code>clientOnly</code> requests don't get
// sent to the server, but the standard callback chain is still invoked.  If all requests in a
// transaction are clientOnly then the callbacks are called immediately otherwise they're
// called when the server returns responses to the non-clientOnly requests.
//
// @visibility internal
//<

//> @attr rpcRequest.params (Object : null : IRW)
//
// Values to be sent as simple HTTP params, as a JavaScript Object where each property/value
// pair will become an HTTP parameter name and value.  These parameters are then accessible on
// the server, for example, using servletRequest.getParameter(paramName) in Java Servlets.
// <P>
// Array-valued parameters will be submitted as multiple instances of the same parameter,
// similar to an HTML form with a multi-select (?paramName=value1&amp;paramName=value2 ...),
// accessible as getParameterValues(paramName) in Java Servlets.  Any non-atomic type, such as
// an Object, will be serialized to +externalLink{http://www.json.org/,JSON} by the
// +link{JSONEncoder}.  If this isn't desirable, serialize the data in advance so that the
// value provided in <code>rpcRequest.params</code> is a String.
// <P>
// Note that this API is primarily used in combination with +link{rpcRequest.useSimpleHttp} -
// when contacting the SmartClient Server, use +link{RPCRequest.data} instead, which provides
// full JavaScript &lt;-&gt; Java translation of arbitrary structures.
// <code>rpcRequest.params</code> can also be used with the SmartClient Server, where it
// provides an an opportunity to send additional data aside from the main
// +link{rpcRequest.data} payload.  This is useful for adding data to DataSource requests which
// will be kept separate from the automatically sent DataSource data, or for making parts of
// the request visible in the URL for HTTP-level logging or layer 4 switches.
// <P>
// Note that in contrast to +link{rpcRequest.data} object, the data in
// <code>rpcRequest.params</code> is not deserialized by the SmartClient server, and
// all values arrive on the server as String type (like HTTP parameters always do).
// <p>
// <smartclient>
// The params value can also be specified as a componentID or component instance that provides
// a method getValues() that returns an Object containing parameter names and values.
// SmartClient components +link{class:DynamicForm}, +link{class:ValuesManager} are two such
// classes.  Lastly, you may specify the ID of a native form element (retrievable via
// getElementById()) and the params will be populated from there.  If there is an error
// resolving your params directive, it will be logged to the Developer Console.
// </smartclient>
// <p>
// Note: The params are submitted once per http transaction.  If you are using
// +link{RPCManager.startQueue(),request queuing} to bundle multiple RPCRequests or DSRequests
// into a single HTTP turnaround, the params from the various RPCRequests will be merged,
// with the later-queued transactions winning on parameter name collisions.  A warning will be
// logged in the Developer Console if multiple RPCRequests specified params.
//
// @visibility external
//<

//> @attr rpcRequest.evalResult (Boolean : false : IRWA)
//
// This works similarly to +link{RPCRequest.serverOutputAsString} except the resulting String
// is automatically evaluated as JavaScript.  The result of the evaluation is then passed to
// any specified +link{RPCRequest.callback} as +link{RPCResponse.data}.
// <p>
// This feature can be used to dynamically load new application modules into a running
// application.  An RPCRequest with <code>evalResult</code> enabled can be used to fetch a
// static .js file or JavaScript dynamically generated by the server.  The returned JavaScript
// can contain anything that a JavaScript file loaded at init time can contain, including new
// views and new SmartClient class definitions.
// <p>
// <i>Example usage with +link{RPCManager.sendRequest()}:</i>
// <pre>
// isc.RPCManager.sendRequest({
//     evalResult:true,
//     actionURL:"js/loadLabel.js",
//     evalVars:{var1:"A Value"}
// });
// </pre>
// This call would execute the code from <code>loadLabel.js</code>, and make the variable
// <code>var1</code> available to that code. Therefore if the .js file contained this code:
// <pre>
// isc.Label.create({
//     contents:var1
// })
// </pre>
// A label would be created with contents set to the value of <code>var1</code> - the string
// <code>"A Value"</code>.
//
// <p>
// This feature relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
//
// @see class:ViewLoader
// @see rpcRequest.evalVars
// @group viewLoading
// @visibility external
//<

//> @attr rpcRequest.evalVars (Object : null : IRWA)
//
// If you've set +link{RPCRequest.evalResult} : true, then the property values of this object
// will be available in the evaluation scope of the result under the variable names
// specified by the property names.
// <p>
// So e.g. if evalVars is: <code>{foo: "bar"}</code> then a reference to the
// variable <code>foo</code> in the result will evaluate to <code>"bar"</code>.
//
// @group viewLoading
// @visibility external
//<

//> @attr rpcRequest.callbackParam (String : "callback" : IRW)
//
// For use only with the +link{type:RPCTransport,scriptInclude} transport, this attribute
// specifies the name of the URL parameter which is used to specify the callback function that
// the server is expected to call by writing out JavaScript code.  The actual function to call
// is automatically generated and differs for every request (to allow concurrency).
// <P>
// For example, with <code>callbackParam</code> set to it's default value of "callback", the
// server might be contacted with a URL like:
// <pre>
//    loadData?callback=isc_scriptIncludeCallback_5
// </pre>
// .. then the server's response should look like:
// <pre>
//    isc_scriptIncludeCallback_5({ .. data .. });
// </pre>
// The name "isc_scriptIncludeCallback_5" is automatically generated and will differ each time
// the server is contacted.
// <P>
// SmartClient makes of this server-provided callback mechanism, then calls
// +link{rpcRequest.callback} normally.
// <p>
// <code>rpcRequest.callbackParam</code> is ignored by all transport other than
// <code>scriptInclude</code>.
//
// @visibility external
//<

//> @attr rpcRequest.suppressAutoDraw (Boolean : true : IRWA)
//
// If +link{attr:RPCRequest.evalResult} is set, setting this property to true causes
// +link{attr:Canvas.autoDraw} to be set to false for the duration of the result evaluation -
// which is generally what you want if you're returning new components from the server.
// <P>
// This also effects components loaded via the +link{RPCManager.loadScreen} API.
//
// @visibility external
//<

//> @attr rpcRequest.serverOutputAsString (Boolean : false : IRWA)
//
// Setting this flag makes the body of the HTTP response available as a String in the
// +link{RPCRequest.callback} as +link{RPCResponse.data}.  This is typically only useful if you
// are sending a request that will <b>not</b> be received by the SmartClient Java Server,
// however in that case, set +link{useSimpleHttp}:true instead, which implies
// <code>serverOutputAsString:true</code>.
// <P>
// <code>serverOutputAsString:true</code> allows you to, for example, load the contents of
// static files off your webserver into a string for processing on the client with no server
// support.  The +link{RPCRequest.actionURL} must be in the same domain as the current page for
// this to work.
// <p>
// This feature relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
// <p>
// Generally this API is used for either +link{group:nonJavaBackend,non-Java backends}
// or for advanced usage such as content that requires processing before it can be used in
// SmartClient components (such as client-side web scraping).  Note that SmartClient provides
// higher-level APIs for loading common types of data, see eg +link{HTMLFlow} for HTML content,
// +link{ViewLoader} for loading SmartClient components, +link{XMLTools.loadXML()} for loading
// XML, +link{RPCRequest.evalResult} for loading +externalLink{http://www.json.org/,JSON}, and
// +link{DataSource} for loading structured data in various formats.
//
// @visibility external
//<

//> @attr RPCRequest.allowIE9Leak (boolean : null : IRA)
// Advanced flag to avoid a potential memory leak in Internet Explorer 9 for requests
// with JSON formatted responses.
// <P>
// This attribute may be set to <code>false</code> to explicitly enable the
// workaround described +link{RPCManager.allowIE9Leak,here} for this request,
// avoiding a potential memory leak in Internet Explorer 9.
// <P>
// This workaround has a limitation in that if parsing the JSON response generates
// certain object types including JavaScript <code>Date</code> or <code>function</code>
// objects, attempts to interact with these objects can subsequently lead to a
// JavaScript error with the message <code>"Can't execute code from a freed script"</code>.
// <P>
// This workaround therefore may not be suitable for all transactions or dataSources
// within a given application.
// <P>
// This property may also be set globally within an application (via
// +link{RPCManager.allowIE9Leak})_.
// <P>
// Note: This memory leak and workaround is discussed further in the online
// <a href="http://forums.smartclient.com/showthread.php?t=8159">SmartClient FAQ</a>.
//
// @visibility external
//<

//> @attr rpcRequest.transport (RPCTransport : RPCManager.defaultTransport : IRWA)
//
// Selects the transport used for this RPCRequest.  If unset, the value of
// +link{RPCManager.defaultTransport} will be used.
// <p>
// If you're using queueing, note that all requests in the queue must use the same transport.
// If you attempt to send a request via a different transport than those that are currently on
// the queue, it will be sent to the server separately, ahead of the queue, and a warning will
// be logged to the Developer Console.
// <p>
// If you specify an unknown transport, an error will be logged to the DeveloperConsole and
// +link{RPCManager.defaultTransport} will be used instead.
// <p>
// If you specify the <code>xmlHttpRequest</code> transport and it is not available, a warning will be
// logged to the Developer Console and the RPCManager will attempt to use the
// <code>hiddenFrame</code> transport instead for this request.  Note that some features like
// +link{RPCRequest.serverOutputAsString} require the <code>xmlHttpRequest</code> transport and will not
// work if the <code>xmlHttpRequest</code> transport is unavailable (this can happen if the end user is
// using Internet Explorer and has disabled ActiveX).  You can check whether or not the
// <code>xmlHttpRequest</code> transport is currently available by calling
// +link{RPCManager.xmlHttpRequestAvailable}.
//
// @see RPCManager.defaultTransport
//
// @visibility external
//<

//> @attr rpcRequest.useXmlHttpRequest (boolean : RPCManager.useXmlHttpRequest : IRWA)
//
// Selects the default http transport for this RPCRequest.  If set to true, this request will use
// XMLHttpRequest for the transport to the server.  If set to false it will use a hidden frame.  If
// left unset, the transport mechanism is determined from the RPCManager default set in
// +link{RPCManager.useXmlHttpRequest}
// <p>
// If you're using queueing, note that all requests in the queue must use the same transport.
// If you attempt to send a request via a different transport than those that are currently on
// the queue, it will be sent to the server separately, ahead of the queue, and a warning will
// be logged to the Developer Console.
// <p>
// If you specify <code>true</code> for this attribute and XMLHttp is not available, a warning
// will be logged to the Developer Console and RPCManager will attempt to use the frames
// transport for this request.  Note that some features like
// +link{RPCRequest.serverOutputAsString} require the XMLHttp transport and will not work if the
// XMLHttp transport is unavailable (this can happen if the end user is using Internet Explorer
// and has disabled ActiveX).  You can query the availability of XMLHttp by calling
// +link{RPCManager.xmlHttpRequestAvailable()}
//
// @deprecated As of SmartClient 5.5, use +link{RPCRequest.transport}.  If you specify a value
// for this property, it will take precedence over +link{RPCRequest.transport}.
//
// @see RPCManager.useXmlHttpRequest
// @see RPCManager.xmlHttpRequestAvailable()
//
// @visibility external
//<


//> @attr RPCRequest.httpMethod (String : "POST" : IRW)
//
// Selects the HTTP method that will be used for the request.  Typical values are "POST" and
// "GET".
// <P>
// The more obscure "PUT", "DELETE" and "HEAD" methods are also valid, however, none of these
// are supported by the Safari browser previous to version 3.0.
//
// @visibility external
//<


//> @attr RPCRequest.contentType (String : "application/x-www-form-urlencoded" : IRW)
//
// Valid with the xmlHttpRequest transport only and only when
// +link{attr:RPCRequest.httpMethod} is set to "POST".
//
// @visibility external
//<

//> @attr RPCRequest.httpHeaders (Object : null : IRW)
// HTTP headers to send, as a Object mapping Header name -> Header value, eg<br>
// { "Content-Type" : "text/xml" }
// <P>
// Valid with the xmlHttpRequest +link{rpcRequest.transport,transport} only.
//
// @visibility external
//<


//> @attr RPCRequest.containsCredentials (Boolean : false : IRWA)
// For use during +link{group:relogin,Relogin}, this property marks this request an attempt to
// login, therefore a response containing the <code>loginRequiredMarker</code> is a normal
// condition and should result in the status code +link{RPCResponse.STATUS_LOGIN_INCORRECT}
// rather than a call to +link{RPCManager.loginRequired(),loginRequired()}.
// <P>
// It is not required to set <code>containsCredentials</code>, however, it does typically
// simplify relogin logic by separating the handling of RPCs that are login attempts from RPCs
// that are not.
//
// @group relogin
// @visibility external
//<

//> @attr RPCRequest.canDropOnDelay (boolean : false : IRWA)
//
// If the transaction containing this request is requested to be delayed for some reason
// (Authentication relogin is one case), then this flag notifies the server that this request does
// not have to be ultimately fulfilled when the transaction is unblocked.<p>
//
// Typically you would set this flag on requests that periodically refresh a component every N
// seconds, so only the last update is important.
//
// @visibility internal
//<

//> @attr RPCRequest.ignoreTimeout (Boolean : false : IRWA)
//
// When set to true, no reply is expected from the server.  However, if a reply is received, it will
// be processed.<p>
//
// Note: setting this to true, forces +link{attr:RPCRequest.sendNoQueue} to <code>true</code> for
// this request.
//
// @visibility external
//<

//> @attr RPCRequest.sendNoQueue (Boolean : false : IRWA)
//
// When set to true, this request is sent to the server immediately, bypassing any current queue.
//
// @visibility external
//<

//> @attr RPCRequest.paramsOnly (boolean : false : IRWA)
//
// When set to true, assume the request is not going to the SmartClient server, and hence send
// a simple HTTP request.  Values specified in +link{attr:RPCRequest.params} are sent to to the
// server as HTTP request parameters.  If +link{httpMethod} method is POST and
// +link{rpcRequest.data} is supplied, it is assumed to be a string to post as the HTTP
// requestBody.
// <p>
// Setting this to true automatically defaults +link{RPCRequest.serverOutputAsString} to true
// as well.
//
// @deprecated As of SmartClient 5.6, use +link{RPCRequest.useSimpleHttp} instead.
// @visibility external
//<

//> @attr RPCRequest.useSimpleHttp (Boolean : false : IRWA)
//
// When set to true, assume the request is not going to the SmartClient server, and hence send
// a simple HTTP request that does not use SmartClient-specific request encoding.
// <P>
// Values specified in +link{attr:RPCRequest.params} are sent to to the server as HTTP request
// parameters.  If +link{httpMethod} is "GET", parameters appear in the request URL, otherwise
// if httpMethod is "POST", parameters are encoded in the request body (exactly like an HTML form
// does).  These parameters are then accessible via typical server-side APIs for retrieving
// HTTP parameters, eg, servletRequest.getParameter(paramName) in Java Servlets.
// <P>
// Note that if +link{httpMethod} method is POST and +link{rpcRequest.data} is supplied,
// +link{rpcRequest.data} is assumed to be a string to post as the HTTP request body, and
// +link{rpcRequest.params} are sent as URL parameters instead.  This usage is for sending
// custom request bodies such as the XML payloads used for SOAP.  In this case,
// +link{rpcRequest.contentType} is typically also set to indicate the content type of the
// request body.
// <p>
// Setting <code>useSimpleHttp</code> to true also automatically sets
// +link{RPCRequest.serverOutputAsString} to true as well.
//
// @visibility external
//<

//> @attr RPCRequest.bypassCache (Boolean : false : IRWA)
//
// For xmlHttp transport + httpMethod: "GET" only, set to true to force a conditional
// GET request even if the browser thinks it has a current cached response.
//
// @visibility external
//<


//> @attr RPCRequest.omitNullMapValuesInResponse (Boolean : false : IRWA)
//
// If enabled, the server omits any key/value pairs in map that have null values from the
// response.  This can reduce the size of the response when many fields have null values.
// <p>
// To enable this globally for all responses you can set RPCManager.omitNullMapValuesInResponse
// in +link{group:server_properties,server.properties}.
//
// @visibility external
//<

//> @attr RPCRequest.downloadResult (Boolean : false : IRWA)
//
// If enabled, causes the RPCRequest to download the requested resource as a file, either
// showing the browser's Save dialog or displaying the file-content in
// +link{rpcRequest.downloadToNewWindow, a new browser window}.
// <P>
// Setting this attribute to true means that no callback will be fired and implies that the
// request will silently use +link{rpcRequest.transport, transport}: "hiddenFrame".
//
// @visibility external
//<

//> @attr RPCRequest.downloadToNewWindow (Boolean : false : IRWA)
//
// When +link{rpcRequest.downloadResult, downloadResult} is true, setting this attribute to
// true causes the content of the downloaded file to be displayed in a new browser window.
//
// @visibility external
//<

// ---------------------------------------------------------------------------------------
//> @method Callbacks.RPCCallback
// A +link{type:Callback} to evaluate when an RPCRequest completes.
// <smartclient><p>
// Parameters passed to this callback are:
// <ul>
// <li>rpcResponse: an +link{class:RPCResponse} encapsulating the server response to your
//     request
// <li>data: just the "data" property from the RPCResponse, for convenience
// <li>rpcRequest: the +link{class:RPCRequest} that was sent.  You can use
//     +link{attr:rpcRequest.clientContext} to track state during the server turnaround.
// </ul>
// For example, to take the data returned by the server and display it in a previously created
// ListGrid with the ID "myGrid":
// <pre>
//     isc.RPCManager.send("getData", "myGrid.setData(data)");
// </pre>
// Or
// <pre>
//     isc.RPCManager.send("getData", function (rpcResponse, data, rpcRequest) {
//                                        myGrid.setData(data)
//     });
// </pre></smartclient>
//
// @param response (RPCResponse) response a RPCResponse encapsulating the server response to your request
// @param rawData  (any) rawData The "data" property from the RPCResponse, for convenience.  The data can
// also be obtained via {@link RPCResponse#getDataAsMap()}, {@link RPCResponse#getDataAsString()},
// or {@link RPCResponse#getDataAsObject()}, depending on the type of data that is expected to be
// returned from the server.
// @param request (RPCRequest) the RPCRequest that was sent.
//
// @see class:RPCRequest
// @see class:RPCResponse
// @visibility external
//<



// ---------------------------------------------------------------------------------------
//>    @class    RPCResponse
//
// Encapsulates an RPC response from the server.  Instances of this class are automatically created
// and optionally passed to you in the callback you specify as part of your RPCRequest.
//
// @see class:RPCRequest
// @see method:Callbacks.RPCCallback
// @visibility external
// @treeLocation Client Reference/RPC
//<
isc.ClassFactory.defineClass("RPCResponse");

isc.RPCResponse.addClassMethods({
    //> @classMethod RPCResponse.create()
    // RPCResponses shouldn't be created directly. Instances of this class are automatically
    // created and optionally passed to you in the callback you specify as part of your
    // +link{RPCRequest}.
    //
    // @visibility external
    //<
    // Log a warning if called directly
    create : function (A,B,C,D,E,F,G,H,I,J,K,L,M) {
        this.logWarn("RPCResponses shouldn't be created directly. Instances of this class " +
                     "are automatically created and optionally passed to you in the callback " +
                     "you specify as part of your RPCRequest.");

        return isc.addProperties({}, A,B,C,D,E,F,G,H,I,J,K,L,M);
    }
});

//> @attr rpcResponse.data (String | Object : false : R)
// The data sent by the server.
// <P>
// When communicating with the SmartClient server, rpcResponse.data is the data passed to the
// server-side method RPCResponse.setData() by your Java code.
// <smartclient>This data is translated into JavaScript
// objects by the rules described under +link{rpcRequest.data}.</smartclient>
// <smartgwt>This data is translated into JavaScript
// objects by the rules described under +link{rpcRequest.data}. Simple types (Numeric values,
// Strings, Dates, Booleans) will be available as their equivalent Java types in your
// client side GWT code. Complex objects (such as serialized Maps or Lists from the server)
// will not be automatically translated back into Java on the client - they will arrive as
// <code>JavaScriptObject</code> instances. You can easily convert
// to the appropriate type yourself using the <code>JSOHelper</code> class. The
// +sgwtLink{JSOHelper.convertToJava()} method performs a recursive conversion of JavaScriptObjects
// returning a List (or array) for JavaScript arrays or a Map for simple
// JavaScript objects (key:value pairs).
// </smartgwt>
// <P>
// When not communicating with the SmartClient server rpcResponse.data contains the
// raw HTTP response body. See +link{rpcRequest.useSimpleHttp},
// +link{rpcRequest.serverOutputAsString}, +link{rpcRequest.evalResult} for details.
// @visibility external
//<



// Server->client conversion follows the this table as well, with some extras.  See the toJS()
// method on JSTranslater in the server documentation for a description of additional
// behaviors.
// <P>



//> @attr rpcResponse.status (int : : IR)
//
// Status code for this response.  Status codes less than zero are considered errors by the
// RPCManager, those greater than or equal to zero are considered successes.  Please see the
// error handling section the +link{class:RPCManager,RPCManager docs} for more information on
// what the RPCManager does with the status code and how you can override this behavior.
// <P>
// When using the SmartClient server you can set the rpcResponse.status by calling the
// server-side method RPCResponse.setStatus().
// <P>
// When not using the SmartClient server, the RPCManager makes no assumptions about the
// structure of the response, so the status code just reflects the
// +link{attr:RPCResponse.httpResponseCode}: status will be
// +link{RPCResponse.STATUS_TRANSPORT_ERROR,STATUS_TRANSPORT_ERROR} if an HTTP-level error
// occurred such as "500 server error".  If you have a status code you need to transmit you can
// simply embed it in the response (as part of +link{rpcResponse.data}) and interpret it from
// the callback.
// <P>
// With or without the SmartClient server, the +link{group:relogin} status codes (such as
// +link{STATUS_LOGIN_REQUIRED}) are triggered whenever special markers, such as the
// loginRequiredMarker, appear in the body of the response.  See the +link{group:relogin,Relogin
// Overview} for details.
//
// @visibility external
//<

//> @attr rpcResponse.httpResponseCode (integer : null : R)
//
// This attribute (available when using the the <code>xmlHttpRequest</code> transport) contains
// the HTTP response code sent by the server.
// <p>
// Note that this is different from +link{attr:RPCResponse.status} - that attribute is used to
// indicate a status code for the RPC itself whereas httpResponseCode is the raw HTTP response
// code for the HTTP request that contained the RPCRequest.
// <p>
// This feature relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
// <p>
// If you're using this attribute, you'll typically want to avoid the default error
// handling response of RPCManager.  To do so, set
// +link{attr:rpcRequest.willHandleError} to <code>true</code>.
//
// @visibility external
//<

//> @attr rpcResponse.httpHeaders (Object : null : R)
// HTTP headers returned by the server as a map from header name to header value.
// <P>
// Headers are available only when the default +link{RPCTransport} "xmlHttpRequest" is in use,
// and browsers may limit access to headers for cross-domain requests or in other
// security-sensitive scenarios.
//
// @visibility external
//<

//> @attr rpcResponse.httpResponseText (String : null : R)
// The actual text of the HTTP response.  Only available when the default
// +link{RPCTransport} "xmlHttpRequest" transport is in use,
// @visibility external
//<

//> @attr rpcResponse.clientContext (Object : null : R)
//
// The +link{RPCRequest.clientContext} object as set on the +link{RPCRequest}.
//
// @see rpcRequest.clientContext
//
// @visibility external
//<

//> @attr rpcResponse.transactionNum (int : null : R)
// ID of the transaction sent to the server via +link{RPCManager.sendQueue()} containing the
// +link{RPCRequest} associated with this response.
// @visibility external
//<

//> @type RPCTransport
//
// SmartClient supports multiple RPC transports for maximum compatibility and feature richness.
// All of transports use HTTP as the underlying protocol, but use different mechanisms for
// sending the HTTP request and processing the response.  The transport is typically
// auto-selected for by based on the feature being used and the current browser settings.  For
// advanced use cases, +link{RPCRequest.transport} and +link{RPCManager.defaultTransport} are
// exposed as override points.
// <p>
// @value "xmlHttpRequest"  Uses the XMLHttpRequest object to make the request to the server.
// Note that in some browsers with certain configurations, this transport may not be
// available.  See +link{group:platformDependencies} for more information.  This transport is
// not useful with file uploads.  Cannot be used to target cross-domain URLs directly.
//
// @value "scriptInclude"   Write a SCRIPT tag into the DOM with a SRC attribute that targets
// an arbitrary URL.  This transport is the only one that allows direct cross-domain URL
// access.
// <P>
// For +link{rpcRequest.callback} to work, the server being contacted must support the ability
// to generate JavaScript code in the response that will call a JavaScript function generated
// by SmartClient.  SmartClient passes the name of the function to call via a URL parameter,
// which can be controlled with +link{rpcRequest.callbackParam}.  This callback mechanism is
// sometimes called the "JSONP" (JSON with Padding) approach.
//
// @value "hiddenFrame"     Available with SmartClient Server only.  An HTML form is
// dynamically assembled that targets a hidden IFRAME.  This mechanism is supported on all
// browsers and cannot be disabled by end users.
// <P>
// If using the SmartClient Server and using
// +link{group:serverDataIntegration,Server-side data integration}, the "hiddenFrame" transport
// is automatically used for all RPCManager and DataSource requests if the "xmlHttpRequest"
// transport is not available.
// <P>
// Cannot be used to target cross-domain URLs directly.
//
//
// @visibility external
//<

//> @groupDef platformDependencies
//
// Client-side processing of web services, XML parsing, and some UI loading mechanisms rely on
// a native in-browser XML parser and/or the XMLHttpRequest object - one or both of which will
// not be available if the end user disables ActiveX support in Internet Explorer.  Note that
// these features do not require plugins or downloads of any kind - IE simply exposes certain
// built-in functionality like the XML parser and XMLHttpRequest through the ActiveX
// interface.  Disabling ActiveX also disables all browser plugins such as Flash, Java, SVG, etc.
// <p>
// Barring ActiveX being disabled, the XMLHttpRequest object is available to SmartClient on all
// supported browsers and an XML parser is available on all supported browsers except Safari
// versions prior to 3.0.3.
// <p>
// SmartClient client-server communication is not affected by the lack of an XML parser or the
// XMLHttpRequest object, but the <code>xmlHttpRequest</code> transport will not be available
// if the XMLHttpRequest object is not available.  Instead, the <code>hiddenFrame</code> or the
// <code>scriptInclude</code> transports are used for client-server communication.
// <p>
// <b><u>XML Parser</u></b>
// <p>
// If an XML Parser is not available to SmartClient, all client-side web service bindings and
// related methods will be unavailable.  Turning off ActiveX disables integration paths 2 and 3
// in the diagram below.  If you want to bind to web services and require deployment to IE
// without ActiveX (or you need to support Safari pre 3.0.3), you'll need to do all XML processing on the
// server and use either the SmartClient DSRequest or JSON operation pathways (integration
// paths 1 and 4 in the diagram below).  See the discussion in +link{clientServerIntegration}
// for more information on the integration paths shown in the diagram below.
// <p>
// You call +link{XMLTools.nativeXMLAvailable()} to check for the availability of a native XML
// parser at runtime.
// <p>
// <img src="${isc.DocViewer.instance.referenceRoot}skin/ds_bindings.png" width=763 height=475>
// <p>
// <b><u>XMLHttpRequest</u></b>
// <p>
// The XMLHttpRequest object is used for the <code>xmlHttpRequest</code> +link{RPCTransport}.
// Safari, Mozilla, Firefox, and IE 7 provide a native XMLHttpRequest implementation that is
// not affected by ActiveX being disabled (although the native IE 7 implementation can still be
// explicitly disabled by the end user).  IE 5.5 and IE 6.0 rely on the ActiveX bridge to
// support XMLHttpRequest, so if ActiveX is disabled in these browsers, XMLHttpRequest will not
// be available.
// <p>
// The lack of the XMLHttpRequest objects affects UI loading features like +link{ViewLoader},
// and +link{HTMLFlow} when used in remote loading mode (via +link{HTMLFlow.contentsURL},
// +link{HTMLFlow.setContentsURL}, but does not affect the typical client/server communication
// pathways (integration paths 1 and 5 in the diagram above).
// <p>
// Also affected are low level features +link{RPCRequest.serverOutputAsString},
// +link{RPCRequest.evalResult}, and +link{RPCResponse.httpResponseCode}.
// <p>
// In all of the above cases, it is possible to use the <code>hiddenFrame</code> transport to
// support these features when XMLHttpRequest is not available.  SmartClient will automatically
// send the request using the <code>hiddenFrame</code> transport when it detects that
// XMLHttpRequest is unavailable.  To support the above features, you'll need to use the
// RPCManager APIs on the server to send back the data that would normally be returned by
// XMLHttpRequest.  Since XMLHttpRequest cannot target URLs outside of the current domain, this
// strategy applies also to using the above features with cross-domain URLs.
// <p>
// You can call +link{RPCManager.xmlHttpRequestAvailable()} to check for the availability of
// XMLHttpRequest at runtime.
//
// @title Platform Dependencies
// @treeLocation /Client Reference/System
// @visibility external
//<

isc.RPCResponse.addClassProperties({
//> @groupDef  statusCodes
// Status codes returned by the server as rpcResponse.status.<br>
// See the error handling doc section in +link{class:RPCManager, RPCManager} for more
// information on these codes
// @visibility external
//<

// NOTE: error codes are both added as a subobject (to allow code -> text name lookup) and
// directly (via addProperties below)
errorCodes : {


    //> @classAttr rpcResponse.STATUS_SUCCESS (integer : 0 : R)
    //
    // Indicates successful completion of the request.  This is the default status and is
    // automatically used by the RPCResponse on the server unless you override it with
    // setStatus().
    // <br><br>
    // See the error handling section in +link{class:RPCManager, RPCManager documentation}
    // for more information.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_SUCCESS: 0,

    //> @classAttr rpcResponse.STATUS_OFFLINE (integer : 1 : R)
    //
    // Indicates that the browser is currently offline, and that we do not hold a cached
    // response for the request.
    //
    // @see class:RPCRequest
    // @group statusCodes, offlineGroup
    // @visibility external
    //<
    STATUS_OFFLINE: 1,

    //> @classAttr rpcResponse.STATUS_FAILURE (integer : -1 : R)
    //
    // Indicates a generic failure on the server.
    // See the error handling section in +link{class:RPCManager, RPCManager documentation}
    // for more information.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_FAILURE: -1,

    //> @classAttr rpcResponse.STATUS_VALIDATION_ERROR (integer : -4 : R)
    //
    // Indicates a validation failure on the server.
    // See the error handling section in +link{class:RPCManager, RPCManager documentation}
    // for more information.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_VALIDATION_ERROR: -4,

    //> @classAttr rpcResponse.STATUS_LOGIN_INCORRECT (integer : -5 : R)
    //
    // Indicates that the RPC has been intercepted by an authenticator that requires the user
    // to log in.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_LOGIN_INCORRECT: -5,

    //> @classAttr rpcResponse.STATUS_MAX_LOGIN_ATTEMPTS_EXCEEDED (integer : -6 : R)
    //
    // Indicates that too many authentication attempts have been made and the server refuses to
    // accept any more login attempts.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_MAX_LOGIN_ATTEMPTS_EXCEEDED: -6,

    //> @classAttr rpcResponse.STATUS_LOGIN_REQUIRED (integer : -7 : R)
    //
    // Indicates that a login is required before this RPCRequest can proceed.
    // <P>
    // Applications do not directly set this status code, instead, to trigger the relogin flow,
    // return the loginRequiredMarker in the response sent by your server when login is
    // required.  See the +link{group:relogin,Relogin Overview} for details.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_LOGIN_REQUIRED: -7,

    //> @classAttr rpcResponse.STATUS_LOGIN_SUCCESS (integer : -8 : R)
    //
    // Indicates that the login succeeded.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_LOGIN_SUCCESS: -8,

    //> @classAttr rpcResponse.STATUS_UPDATE_WITHOUT_PK_ERROR (integer : -9 : R)
    //
    // Indicates that the client attempted an update or remove operation without providing
    // primary key field(s)
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_UPDATE_WITHOUT_PK_ERROR: -9,

    //> @classAttr rpcResponse.STATUS_TRANSACTION_FAILED (integer : -10 : R)
    //
    // Indicates that the request was either never attempted or was rolled back, because
    // automatic or user transactions are in force and another request in the same transaction
    // failed.  Note that the request(s) that actually failed will have a code specific to the
    // failure; it is only the requests that would otherwise have succeeded that are marked
    // with this failure code.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_TRANSACTION_FAILED: -10,

    //> @classAttr rpcResponse.STATUS_MAX_FILE_SIZE_EXCEEDED (integer : -11 : R)
    //
    // Indicates that uploaded file exceeded max file size allowed.
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_MAX_FILE_SIZE_EXCEEDED: -11,

    //> @classAttr rpcResponse.STATUS_MAX_POST_SIZE_EXCEEDED (integer : -12 : R)
    //
    // Indicates that the total size of the data sent to the server was more than the server is
    // configured to allow.  Most servers limit the post size to prevent out of memory style
    // attack vectors that push a bunch of data at the server.  Apache Tomcat, for example,
    // is pre-configured to limit post size to 2mb.
    // <P>
    // On internal networks, these limits can typically be safely raised or removed.  With
    // Tomcat, for example, you can remove the post limit by specifying the following attribute
    // on the &lt;Connector&gt; element in conf/server.xml:
    // <br><pre>
    // maxPostSize="-1"
    // </pre>
    // <p>
    // <b>NOTE</b>: this status code is used whenever the server framework receives a request
    // where the POST data has been removed, however, there are other possible causes,
    // including:
    // <ul>
    // <li> security software installed on the server or network that erroneously detects some
    //      kind of exploit attempt, if its behavior is to just strip the POST data but allow
    //      the rest of the request through (SiteMinder is one product known to do this)
    // <li> incorrectly written filter servlets that drop POST'd data
    // </ul>
    //
    // @see class:RPCRequest
    // @group statusCodes
    // @visibility external
    //<
    STATUS_MAX_POST_SIZE_EXCEEDED: -12,

    //> @classAttr rpcResponse.STATUS_TRANSPORT_ERROR (integer : -90 : R)
    //
    // This response code is usable only with the XMLHttpRequest transport and indicates that
    // the server returned an HTTP response code outside the range 200-299 (all of these statuses
    // indicate success, but ordinarily only 200 is used).  To get the actual
    // response code, you can query rpcResponse.httpResponseCode in your callback.
    // <p>
    // Note that currently this error code will never occur for the <code>hiddenFrame</code>
    // transport - instead, use +link{RPCResponse.STATUS_SERVER_TIMEOUT} to detect
    // <code>hiddenFrame</code> transport errors.
    //
    // @group statusCodes
    // @visibility external
    //<
    STATUS_TRANSPORT_ERROR: -90,

    //> @classAttr rpcResponse.STATUS_UNKNOWN_HOST_ERROR (integer : -91 : R)
    //
    // This response code only occurs when using the HTTP proxy.  It is issued by the proxy
    // servlet when the target host is unknown (ie, cannot be resolved through DNS).  This
    // response probably indicates that you are attempting to contact a nonexistent server
    // (though it might mean that you have DNS problems).
    //
    // @group statusCodes
    // @visibility external
    //<
    STATUS_UNKNOWN_HOST_ERROR: -91,

    //> @classAttr rpcResponse.STATUS_CONNECTION_RESET_ERROR (integer : -92 : R)
    //
    // This response code only occurs when using the HTTP proxy.  It is issued by the proxy
    // servlet when the attempt to contact the target server results in a Java SocketException.
    // This response probably indicates that the target server is currently down.
    //
    // @group statusCodes
    // @visibility external
    //<
    STATUS_CONNECTION_RESET_ERROR: -92,

    //> @classAttr rpcResponse.STATUS_SERVER_TIMEOUT (integer : -100 : R)
    //
    // Indicates a request timed out with no server response.
    // <p>
    // This is a client-only error code - never sent by the server (since it's the server
    // that times out).
    // <p>
    // NOTE that if using <code>hiddenFrame</code> as the transport (not the default), a
    // malformed response such as a "500 Server Error" or 404 errors will be reported as a
    // timeout.
    //
    // @group statusCodes
    // @visibility external
    //<
    STATUS_SERVER_TIMEOUT: -100
}

});
isc.RPCResponse.addClassProperties(isc.RPCResponse.errorCodes);

// alias DSResponse to RPCResponse so that end users can use the response codes as
// e.g. DSResponse.STATUS_SUCCESS
isc.addGlobal("DSResponse", isc.RPCResponse);

isc.RPCManager.addClassProperties({
    // truncate warn() dialog with server error to this many chars
    maxErrorMessageLength: 1000,

    // truncate extra long RPCManager log messages unless RPCManagerResponse has been set
    maxLogMessageLength: 25000,

    //> @classAttr RPCManager.defaultTimeout (int : 240000 : RW)
    // In milliseconds, how long the RPCManager waits for an RPC request to complete before
    // returning an error.
    // <p>
    // Default of 240000 milliseconds is four minutes.  If set to zero, the RPCManager will not
    // enforce a timeout, however, see +link{rpcRequest.timeout} for a discussion of default
    // timeouts that are built into browsers.
    //
    // @visibility external
    //<
    defaultTimeout: 240000,

    //> @groupDef i18nMessages
    // The properties in this group are default system messages that a localized
    // application will want to override on a per-locale basis
    // @see group:i18n
    // @visibility external
    //<

    //> @classAttr RPCManager.defaultPrompt (HTMLString : "Contacting Server..." : IRW)
    //
    // If showPrompt is enabled for a given transaction, this is the defaultPrompt to be shown
    // to the user in a modal dialog while the transaction occurs.
    // May be overridden at the request level via +link{attr:rpcRequest.prompt}.<br>
    // More targetted default prompts are also supported for certain code-paths. See the following
    // set of properties for details:<ul>
    // <li>+link{RPCManager.removeDataPrompt}</li>
    // <li>+link{RPCManager.saveDataPrompt}</li>
    // <li>+link{RPCManager.fetchDataPrompt}</li>
    // </ul>
    //
    // @see classAttr:RPCManager.showPrompt
    // @see classAttr:RPCManager.promptStyle
    // @see classAttr:RPCManager.promptCursor
    // @see attr:rpcRequest.showPrompt
    // @see attr:rpcRequest.prompt
    // @see attr:rpcRequest.promptStyle
    // @see attr:rpcRequest.promptCursor
    //
    // @visibility external
    // @group rpcPrompt
    // @group i18nMessages
    //<
    defaultPrompt:"Contacting server...",

    //> @classAttr RPCManager.timeoutErrorMessage   (HTMLString : "Operation timed out" : IRW)
    // Default message displayed to user when an operation fails to return from the server within
    // the timeout period specified by +link{RPCManager.defaultTimeout}.
    // @see classAttr:RPCManager.defaultTimeout
    // @visibility external
    // @group i18nMessages
    //<
    timeoutErrorMessage:"Operation timed out",

    //> @classAttr RPCManager.removeDataPrompt  (HTMLString : "Deleting Record(s)..." : IRW)
    // Default prompt displayed to user while an operation is running to remove data from
    // the server.<br>
    // Displayed as a result of the +link{ListGrid.removeSelectedData()} code path.
    // @visibility external
    // @group i18nMessages
    //<
    removeDataPrompt:"Deleting record(s)...",


    //> @classAttr  RPCManager.saveDataPrompt   (HTMLString : "Saving form..." : IRW)
    // Default prompt displayed to the user while an operation is running to save data to
    // the server.<br>
    // Displayed as a result of the +link{DynamicForm.saveData()} code path.
    // @visibility external
    // @group i18nMessages
    //<
    saveDataPrompt:"Saving form...",

    //> @classAttr  RPCManager.validateDataPrompt   (HTMLString : "Validating..." : IRW)
    // Default prompt displayed to the user while a server validation is pending.
    // @visibility external
    // @group i18nMessages
    //<
    validateDataPrompt:"Validating...",

    //> @type PromptStyle
    //
    // @value "dialog" Displays a centered modal prompt with text specified by
    //                 +link{rpcRequest.prompt}
    // @value "cursor" Changes the current cursor to the style specified by
    //                 +link{rpcRequest.promptCursor}
    //
    // @visibility external
    //<

    //> @classAttr  RPCManager.promptStyle   (PromptStyle : "dialog" : IRW)
    // Controls the default prompt style.  Overrideable by +link{attr:rpcRequest.promptStyle}.
    //
    // @visibility external
    // @see attr:rpcRequest.promptStyle
    // @group rpcPrompt
    //<
    promptStyle: isc.Dialog ? "dialog" : "cursor",

    //> @classAttr RPCManager.useCursorTracker (boolean : platform-dependent : IRW)
    //
    // If true, an image is shown to the right of the cursor when +link{rpcRequest.promptStyle} is
    // set to "cursor", otherwise the cursor itself is modified via css to the value of
    // +link{rpcRequest.promptCursor}.  The default is platform-dependent.  In IE 5.5 and
    // Firefox 1.0 the default is true, on all other platforms it is false.  The reason for
    // this split is that, the above browsers require that the cursor move before CSS settings are
    // re-evaluated - this means the progress cursor can stick until the user moves the mouse.
    // <p>
    // This value can be overridden on a per-request basis via +link{rpcRequest.useCursorTracker}.
    //
    // @see rpcRequest.useCursorTracker
    // @group rpcPrompt
    // @visibility external
    //<
    useCursorTracker: (isc.Browser.isMoz && isc.Browser.geckoVersion < 20051111),
    cursorTrackerConstructor: "Img",
    cursorTrackerDefaults: {
        src : "[SKINIMG]shared/progressCursorTracker.gif",
        size: 16,
        offsetX: 12,
        offsetY: 0,
        _updatePosition : function (init) {
            var left = (isc.EH.getX()+this.offsetX),
                top = (isc.EH.getY()+this.offsetY);

            // hide the cursorTracker when we hit the right or bottom edge of the browser so it
            // doesn't cause overflow and introduce scrolling
            if (left+this.size >= isc.Page.getWidth() || top+this.size >= isc.Page.getHeight()) {
                this.hide();
                return;
            }

            // we've seen cases where we can get non numeric values here - in this case
            // calling setLeft/top can mess up sizing of the wait img
            if (isNaN(left)) left = 0;
            if (isNaN(top)) top = 0;
            this.setLeft(left);
            this.setTop(top);
            if (!init && !this.isVisible()) this.show();
        },
        initWidget : function () {
            this.Super("initWidget", arguments);
            this._updatePosition(true);
            this._updateEvent = isc.Page.setEvent("mouseMove", this.getID()+"._updatePosition()");
            this._mouseOutEvent = isc.Page.setEvent("mouseOut", this.getID() + ".hide()");
            this.bringToFront();
        },
        destroy : function () {
            isc.Page.clearEvent("mouseMove", this._updateEvent);
            isc.Page.clearEvent("mouseOut", this._mouseOutEvent);
            this.Super("destroy", arguments);
        }
    },

    //> @classAttr  RPCManager.promptCursor   (String : browser-dependent : IRW)
    // Controls the default cursor shown when +link{rpcManager.promptStyle} is set to
    // <code>"cursor"</code>.  Overrideable by +link{attr:rpcRequest.promptCursor}.
    // <p>
    // In Safari, IE 5.5 and Firefox 1.0 the default value is "wait", on all other platforms it is
    // "progress".  The reason for this split is that the above-mentioned browsers do not support
    // CSS2.1 - which is required for the "progress" cursor type.
    //
    // @visibility external
    // @see attr:rpcRequest.promptCursor
    // @group rpcPrompt
    //<
    // FF1.0, Safari don't support CSS2.1, so must use 'wait' - otherwise 'progress'
    promptCursor: isc.Browser.isSafari ||
        (isc.Browser.isMoz && isc.Browser.geckoVersion < 20051111) ||
        (isc.Browser.isIE && isc.Browser.minorVersion <= 5.5) ? "wait" : "progress",

    //> @classAttr  RPCManager.fetchDataPrompt  (string : "Finding Records that match your criteria..." : IRW)
    // Default prompt displayed to the user while an operation is running to fetch data from
    // the server.<br>
    // Displayed as a result of +link{ListGrid.filterData()}, +link{ListGrid.fetchData()} and
    // +link{ListGrid.clearCriteria()} code paths.
    // @visibility external
    // @group i18nMessages
    //<
    fetchDataPrompt:"Finding records that match your criteria...",


    // Hidden prompts (not currently used by any exposed code path)
    // ---------------------------------------------------------------

    getViewRecordsPrompt:"Loading record...",

    //> @classAttr RPCManager.showPrompt (boolean : RPCManager.showPrompt : RW)
    // If set to <code>true</code>, the RPCManager will block the UI with a modal dialog containing
    // the text from RPCManager.defaultPrompt (or the per-RPCRequest override) until the RPC to the
    // server completes.
    // <p>
    // If set to <code>false</code>, the RPC happens transparently, allowing the user to continue
    // interacting with the UI.
    // <p>
    // DataSource requests, which are a particular type of RPCRequest, are controlled by the
    // more-specific DataSource-level setting +link{DataSource.showPrompt}.
    //
    // @see classAttr:RPCManager.defaultPrompt
    // @see attr:rpcRequest.showPrompt
    // @group rpcPrompt
    // @visibility external
    //<
    showPrompt: false,

    //> @classAttr RPCManager.promptDelay (number : 300 : IRWA)
    //
    // If the request is configured to block user interactivity (+link{rpcRequest.showPrompt}),
    // this property controls the delay in milliseconds before a visual indication is shown to the
    // user that interactivity is blocked.
    // <P>
    // Studies have shown that users will perceive a short operation as occurring faster if they
    // are not shown a wait cursor, throbber or other busy indicator, but that a busy indicator
    // <i>must</i> appear after a briefy delay or the user will perceive the system as broken or
    // hung.
    // <P>
    // Note that, regardless of this setting, interactivity is immediately blocked if showPrompt
    // is true, since the purpose of blocking is to prevent duplicate requests or prevent
    // interacting with components while they are in transition.  This setting controls only how
    // fast a visual indication of blocking is shown.
    //
    // @see classAttr:RPCManager.showPrompt
    // @see attr:rpcRequest.promptDelay
    // @group rpcPrompt
    // @visibility external
    //<
    promptDelay: 300,


    neverShowPrompt: false,

    //> @classAttr RPCManager.actionURL (URL : RPCManager.actionURL : RW)
    // Specifies the default URL for RPCRequests and DSRequests that do not specify a
    // URL.
    // <p>
    // URLs can be set on a per-request basis via +link{rpcRequest.actionURL}, or on a
    // per-DataSource or per-operationType basis via +link{DataSource.dataURL} and
    // +link{operationBinding.dataURL} respectively.  However, note that in order to be able to
    // make use of +link{RPCManager.startQueue,queuing}, you should have all data loading and
    // saving requests go to a single URL unless you are forced to use distinct URLs by legacy
    // services.
    // <p>
    // The primary use case for setting the default <code>actionURL</code> is to add a CSRF / XSRF
    // (+externalLink{http://en.wikipedia.org/wiki/Cross-site_request_forgery,Cross-site Request Forgery})
    // token.  Assuming you are using a single URL for all data requests as covered above,
    // adding a CSRF token to the default <code>actionURL</code> as a simple HTTP parameter
    // will cause the CSRF token to be included in all RPCRequests and DSRequests from all
    // DataSources without further effort.
    //
    // @visibility external
    //<
    actionURL:"[ISOMORPHIC]/IDACall",

    //> @classAttr RPCManager.screenLoaderURL (URL : RPCManager.screenLoaderURL : RW)
    //
    // The screenLoaderURL specifies the URL where ScreenLoaderServlet is installed.
    //
    // @visibility external
    //<
    screenLoaderURL:"[ISOMORPHIC]/screenLoader",

    //> @classAttr RPCManager.ALL_GLOBALS (string : "-ALL_GLOBALS" : R)
    //
    // Passing this special value to +link{RPCManager.loadScreen,loadScreen} indicates that
    // all global names should be preserved when evaluating loaded screen.<br/>
    //
    // @see RPCManager.loadScreen
    // @visibility external
    //<
    ALL_GLOBALS: "-ALL_GLOBALS",

    //> @classAttr RPCManager.useXmlHttpRequest (boolean : true if XMLHttpRequest is supported, false otherwise : RW)
    //
    // Selects the default http transport for all RPC requests.  If set to true, RPCManager
    // will use XMLHttp for requests to the server.  If set to false, it will use hidden
    // frames.  Overrideable on a per-request basis via +link{RPCRequest.useXmlHttpRequest}.
    // <p>
    // Note that if the end user disables ActiveX controls in Internet Explorer, the XMLHttpRequest
    // object will not be available and SmartClient will automatically fall back on frames
    // communication.
    //
    // @deprecated As of SmartClient 5.5, use +link{RPCManager.defaultTransport}.  If you
    // specify a value for this property, it will take precedence over
    // +link{RPCManager.defaultTransport} for requests that do not specify a
    // +link{RPCRequest.transport} or +link{RPCRequest.useXmlHttpRequest}.
    //
    // @see attr:rpcRequest.useXmlHttpRequest
    //
    // @visibility external
    //<

    //> @classAttr RPCManager.defaultTransport (RPCTransport : "xmlHttpRequest": IRW)
    //
    // Selects the transport use for RPC requests by default.  You can override this setting on
    // a per-request basis by setting +link{rpcRequest.transport}.
    //
    // @see rpcRequest.transport
    // @visibility external
    //<
    defaultTransport: "xmlHttpRequest",

    //> @classAttr RPCManager.useHttpProxy (Boolean : true : IR)
    // Whether the +link{group:servletDetails,HttpProxyServlet} should be used in order to get
    // around the "same origin policy" that prevents web pages from contacting other sites.
    // <p>
    // Default behavior is to use the HttpProxyServlet whenever a URL appears to be pointing to
    // another site.  Set +link{rpcRequest.useHttpProxy} false to have a particular request
    // avoid using the HttpProxyServlet even when it appears to be necessary, or set
    // <code>RPCManager.useHttpProxy</code> to false to avoid ever attempting to use the
    // HttpProxyServlet.
    //
    // @visibility external
    //<

    

    //> @classAttr RPCManager.dataEncoding (string : RPCManager.dataEncoding : RWA)
    //
    // Controls the encoding of the _transaction field of the client->server comm.  Valid values are
    // "XML" and "JS" and enable the XML and Javascript encoding, respectively.
    //
    // @visibility internal
    //<
    dataEncoding: "XML",

    //> @classAttr RPCManager.preserveTypes (boolean : RPCManager.preserveTypes : RWA)
    //
    // If true, numbers and booleans become Number and Boolean on the server.  Otherwise they are
    // Strings.  A true value is currently supported only with dataEncoding: XML.
    //
    // @visibility internal
    //<
    preserveTypes: true,

    //> @classAttr RPCManager.credentialsURL (string : RPCManager.credentialsURL : RWA)
    //
    // Specifies URL where credentials should be submitted to attempt relogin when session
    // timeout is encountered during a background RPC.  See +link{group:relogin,Relogin}
    //
    // @group relogin
    // @visibility external
    //<
    credentialsURL: isc.Page.getIsomorphicDir()+"login/loginSuccessMarker.html",

    // XXX document
    loginWindowSettings: "WIDTH=550,HEIGHT=250",

    // don't scan RPC responses longer than this for relogin commands: 1 Megabyte.  Scanning a
    // file this large for the relogin string on a Core 2 Duo 6700 CPU in IE takes 2.66ms
    maxLoginPageLength: 1048576,

    // outstanding transactions to the server and counter.  A transaction is a set of
    // RPCRequests sent to one URL
    _transactions: Array.create({
        // these methods are here for the RPCTracker class that's part of the DevConsole (RPC
        // tracking)
        addTrack : function (object, secondArg) {
            this._lastChanged = object;
            this.add(object, secondArg);
            this._lastChanged = null;
        },
        setLastChanged : function (transaction) {
            this._lastChanged = transaction;
        },
        clearLastChanged : function () {
            this._lastChanged = null;
        },
        getLastChanged : function () {
            return this._lastChanged;
        }
    }),
    _nextTransactionNum: 0,
    _activeTransactions:[],
    getTransactions : function () { return this._transactions; },
    getActiveTransactions : function () { return this._activeTransactions },
    pendingRpcs: 0
});


isc.RPCManager.addClassMethods({

    //> @classMethod RPCManager.queueSent()
    //
    // This method is called by the RPCManager every time it sends a queue of requests to the
    // server (note that if you are not using queuing, the system simply sends queues
    // containing just one request, so this API is valid regardless).<p>
    // There is no default implementation of this method; it is simply an override point.
    // It is intended to be used by user code that needs to be notified when SmartClient sends
    // requests to the server.  Note that the list of +link{class:RPCRequest}s passed to this
    // method is strictly <b>read-only</b>.
    //
    // @param requests (List of RPCRequest) The queue of +link{class:RPCRequest}s that was sent
    //
    // @visibility external
    //<

    //> @method Callbacks.QueueSentCallback
    // <p>This method is called by the RPCManager every time it sends a queue of requests to the
    // server (note that if you not using queuing, the system simply sends queues containing
    // just one request, so this API is valid regardless).
    // <p/>
    // It is intended to be used by user code that needs to be notified when SmartGWT sends
    // requests to the server.  Note that the list of {@link com.smartgwt.client.rpc.RPCRequest}'s passed to this
    // method is strictly <b>read-only</b>.
    //
    // @param response (Array of RPCRequest) array of requests sent
    //
    // @visibility external
    //<


    //> @classMethod RPCManager.xmlHttpRequestAvailable()
    //
    // Returns true if the XMLHttpRequest object is available, false otherwise.  See
    // +link{group:platformDependencies} for more information on when XMLHttpRequest parser may
    // not available and what features are
    // impacted as a result.
    //
    // @return (Boolean) true if XMLHttpRequest is available, false otherwise.
    //
    // @visibility external
    //<
    xmlHttpRequestAvailable : function () {
        // createXMLHttpRequest() actually does a new ActiveXObject() in IE, but
        // we don't cache the result because the user can change ActiveX settings on the fly.
        // This probably won't happen in actual usage, but developers will almost certainly try
        // it this way and it needs to work
        if (isc.Browser.isIE) return (isc.Comm.createXMLHttpRequest() != null);
        return true;
    },

    // return the action URL, dereferencing any special local directories.
    // used internally by the RPCManager for url interpolation
    getActionURL : function () { return isc.Page.getURL(this.actionURL); },

    //> @classMethod RPCManager.send()
    //
    // This method is a convenience wrapper on <code>RPCManager.sendRequest()</code> - it calls
    // through to sendRequest().
    //
    // @param data            (any)           data to be passed to the server
    // @param [callback]      (RPCCallback)   method to call on RPC completion
    // @param [requestParams] (Object)        object literal containing any additional properties
    //                                        you want to set - these will be applied to the
    //                                        RPCRequest object that will be auto-created for you.
    //
    // @see RPCManager.sendRequest()
    // @see class:RPCRequest
    //
    // @visibility external
    //<
    send : function (data, callback, requestParams) {
        var rpcRequest = (requestParams || {});
        isc.addProperties(rpcRequest, {
            data: data,
            callback: callback
        });
        return this.sendRequest(rpcRequest);
    },


    _warnIfXmlHttpRequestUnavailable : function (featureName) {
        if (this.xmlHttpRequestAvailable() || !this.logIsWarnEnabled()) return false;

        var message = "Feature "+featureName+" requires the xmlHttpRequest transport"
                     +" which is not currently available because ActiveX is disabled."
                     +" Please see the 'Features requiring ActiveX or Native support'"
                     +" topic in the client-side reference under Client Reference/System"
                     +" for more information.";
        this.logWarn(message);
        return true;
    },

// sendProxied() : send an RPC through an HTTP Proxy
// ---------------------------------------------------------------------------------------
//> @classMethod RPCManager.sendProxied()
// Send an HTTP request to a remote host, potentially through the HttpProxy servlet installed
// on the SmartClient Server.
// <P>
// This API allows contacting services which are hosted on servers other than the origin server
// if the HttpProxy servlet is enabled on the SmartClient Server.
// <P>
// The HttpProxy will be used if the +link{rpcRequest.actionURL} starts with "http" and uses a
// hostname other than "localhost" or <code>window.location.hostname</code>, or if the port
// number differs, or if <code>request.useHttpProxy</code> is explicitly set.  Otherwise the
// request goes to the origin server (the server that returned the current page).
// <P>
// The +link{RPCRequest} properties that will be respected when relaying requests via the
// HttpProxy are:
// +link{RPCRequest.actionURL,actionURL}, +link{RPCRequest.httpMethod,httpMethod},
// +link{RPCRequest.params,params}, +link{RPCRequest.contentType,contentType},
// +link{RPCRequest.httpHeaders,httpHeaders}, and +link{RPCRequest.data,data}.  In this case
// "data", if set, will be used as the request body for an HTTP POST.
// <P>
// Higher-level APIs like +link{DataSource} or +link{WebService} call through this API, and so
// automatically use the HttpProxy if +link{dataSource.dataURL} or
// +link{webService.setLocation(),webService.location} is set to a foreign server.
// <P>
// This API is only suitable for direct use when loading unstructured data that will not be
// shown in a +link{DataBoundComponent}.  For a WSDL-described web service, use
// +link{XMLTools.loadWSDL()} instead.  For other web services, use a +link{DataSource} with
// +link{DataSource.dataURL,dataURL}, and use +link{DataSource.transformRequest()} and
// +link{DataSource.transformResponse()} as necessary to form requests for the service and
// transform responses for display.
//
// @param request (RPCRequest Properties) rpcRequest to be routed through the HttpProxy
// @requiresModules SCServer
// @visibility external
//<
sendProxied : function (request, allowRPCFormat) {


    request.serverOutputAsString = request.transport != "scriptInclude";
    if (!request.isRestRequest) request.sendNoQueue = true; // don't use ISC-format multi-op

    var url = request.actionURL || isc.RPCManager.actionURL;
    //this.logWarn("url is: " + url);
    // use the proxy if request.useHttpProxy has been specifically set for this request, or..
    var useProxy = (request.useHttpProxy != null ? request.useHttpProxy :
            // the proxy is available and ..
            (isc.RPCManager.useHttpProxy &&
                // and the URL appears to be remote (starts with http and not obviously local)
                url.startsWith("http") && !this.isLocalURL(url)));

    
        if (!isc.RPCManager.allowCrossDomainCalls) {
            if (!useProxy && url.startsWith("http") && !this.isLocalURL(url)) {
                isc.warn("SmartClient can't directly contact URL '" + url + "' due to " +
                    "browser same-origin policy.  Remove the host and port number " +
                                    "(even if localhost) to avoid this problem, or use XJSONDataSource " +
                                    "for JSONP protocol (which allows cross-site calls), or use the " +
                                    "server-side HttpProxy included with SmartClient Server." +
                                    "<BR>" +
                                    "This warning may be suppressed by setting " +
                                    "<b>RPCManager.allowCrossDomainCalls</b> to true.");
            }
        }
    

    if (!useProxy)
    {
        // contact origin server directly, but don't send the ISC-specific transaction
        // structure
        if (!allowRPCFormat) request.useSimpleHttp = true;

    } else {
        // contact foreign server by way of HttpProxy, which expects an RPCRequest where "data"
        // contains configuration for sending an HTTPRequest
        var proxyRequest = isc.addProperties({}, request, {
            actionURL : isc.XMLTools.httpProxyURL,
            // mark as proxied so we can perform better error reporting
            isProxied: true,
            // data is parameters the HttpProxy understands
            useSimpleHttp: true,
            proxiedURL: url,
            params: {
                data: null
            },
            // Force XHR on for this request because the proxy will feed us the response
            // directly and this is the only way to capture it without additional server
            // support.  Except, of course for the scriptInclude transport.
            transport: request.transport == "scriptInclude" ? "scriptInclude" : "xmlHttpRequest",
            // wipe out these properties since they apply only to the relayed request, not to
            // the request intended for the HttpProxy servlet.  NOTE: we leave httpHeaders
            // intact in case someone has some kind of advanced usage in mind; RPCManager
            // ignores all headers by default
            httpMethod: null, data: null, contentType:null
        });
        isc.Comm._explicitNils = true;
        proxyRequest.params.data = isc.Comm.xmlSerialize("data", {
            url: url,
            httpMethod: request.httpMethod,
            params: request.params,
            contentType: request.contentType,
            requestBody: request.data,
            username: request.username,
            password: request.password,
            // NOTE: the only header the proxy actually supports sending through is
            // "SOAPAction", but we send all headers to the server in case someone wants to
            // customize this
            httpHeaders: request.httpHeaders,
            uploadFileName: request.uploadFileName,
            // if we're using the scriptInclude transport, pass through the
            // callbackParam to the proxy so that it can push this value through to the
            // proxied target
            callbackParam: request.transport == "scriptInclude" ? request.callbackParam : null
        });
        isc.Comm._explicitNils = null;
        request = proxyRequest;
        //this.logWarn("proxied request: " + this.echo(request) +
        //             ", data for proxy: " + this.echo(request.data) +
        //             ", requestBody: " + this.echo(request.data.requestBody));
    }
    return isc.rpc.sendRequest(request);
},

//> @classAttr RPCManager.allowCrossDomainCalls (boolean : false : IRWA)
// By default SmartClient will show a warning message on attempted requests to another domain as
// this is usually not supported at the browser level by default due to
// security considerations.
// <P>
// Some browsers now do support cross domain requests through the use of Http Access Control headers
// (See the +externalLink{http://www.w3.org/TR/cors/,W3C Cross-Origin Resource Sharing recommendation}).
// If your application intends to rely on this behavior to perform cross-domain requests,
// you can set <code>allowCrossDomainCalls</code> to true to disable the standard SmartClient
// warning when such calls occur.
// <P>
// Note also that this is typically not an issue if you are using the SmartClient server
// (part of Pro, Power and Enterprise editions of SmartClient), as this includes the
// +link{RPCManager.sendProxied,HTTPProxy servlet}.
// @visibility external
//<
allowCrossDomainCalls:false,

// given a URL, get the host without port
_getHostAndPort : function (url) {
    var protocol = isc.Page.getProtocol(url),
        // first slash after the protocol
        endHostSlash = url.indexOf("/", protocol.length),
        host = url.substring(protocol.length, endHostSlash),
        port
    ;
    var colIndex = host.indexOf(":");
    if (colIndex != -1) {
       port = host.substring(colIndex+1);
       host = host.substring(0, colIndex);
    }

    return [host, port];
},

// see if this is a URL that we can access locally
isLocalURL : function (url) {
    var hostAndPort = this._getHostAndPort(url),
        host = hostAndPort[0],
        port = hostAndPort[1]
    ;
    if (port == null || port == "") port = 80;

    // NOTE: bad case: might be accessing wrath.isomorphic.com as just "wrath", in
    // which case we can't detect that wrath.isomorphic.com is actually a local URL.
    // To make this check better we might need to actually attempt an XMLHttpRequest for the
    // ambiguous cases, catch the error, and cache the URL as known good or bad.  However
    // depending on security settings, attempting to access a foreign URL may launch a
    // confirmation dialog, so the best we can do is probably to try to detect whether the
    // HttpProxy servlet is installed (whether via a flag dumped by the loadISC tag or a
    // dynamic request to the server), and assume direct access if its missing
    var liveLocation = this.getWindow().location,
        liveHost = liveLocation.hostname,
        livePort = liveLocation.port;
    if (livePort == null || livePort == "") livePort = 80;

    return (host == "localhost" || host == liveHost)
           && port == livePort
    ;

    // Theoretically document.domain would allow xmlHttpRequests throughout a domain,
    // but quick testing reveals that setting document.domain to "isomorphic.com" on a page
    // served from "wrath.isomorphic.com" causes security exceptions in Moz even for attempts
    // to contact "wrath.isomorphic.com", the origin server.  IE also wedges.
    //return this._getHost(url).endsWith(this.getWindow().document.domain);
},

// ---------------------------------------------------------------------------------------


    //> @classMethod RPCManager.sendRequest()
    //
    // Send the passed <code>RPCRequest</code> to the server.  If queuing is in effect, this queues
    // the request instead.
    //
    // @param rpcRequest  (RPCRequest Properties)  RPCRequest to send to the server
    //
    // @visibility external
    //<
    sendRequest : function (request) {
        // handle call to sendRequest with useHttpProxy explicitly set - you're really supposed
        // to call sendProxied() but this is a common mistake.  Checking for the isProxied flag
        // avoids a loop since sendProxied() calls sendRequest() after reformatting the
        // request.
        if (request.useHttpProxy && !request.isProxied) return this.sendProxied(request);

        // we are delaying transactions and this is a periodic polling or similar request that
        // can be ignored in this circumstance, so drop it
        if (request.canDropOnDelay && this.delayingTransactions) return;

        // make a copy of the request to allow callers to re-use the same object, with
        // modifications, when calling sendRequest() - note: shallow copy
        request = isc.addProperties({}, request);

        // default is true, so set it unless it's been explicitly disabled
        if (request.suppressAutoDraw !== false) request.suppressAutoDraw = true;

        // actionURL can also be specified as URL or url
        request.actionURL =
                // NOTE use Page.getURL() to support special directories such as "[APPFILES]"
                isc.Page.getURL(request.actionURL || request.url ||
                                request.URL || this.getActionURL());

        // check if requested transport is available and fall back if necessary
        // ---------------------------------------------------------------------------------------


        // if the request specifies an explicit transport, we use that.  Otherwise, check
        // backcompat APIs first, then use RPCManager.defaultTransport
        var explicitTransport = request.transport;
        if (!explicitTransport) {
            if (request.useXmlHttpRequest != null || this.useXmlHttpRequest != null) {
                // use of backcompat API on RPCRequest or RPCManager
                if (request.useXmlHttpRequest == null) {
                    if (this.useXmlHttpRequest != null) {
                        request.transport = this.useXmlHttpRequest ? "xmlHttpRequest" : "hiddenFrame";
                    } else {
                        request.transport = this.defaultTransport;
                    }
                } else {
                    request.transport = explicitTransport = request.useXmlHttpRequest ? "xmlHttpRequest" : "hiddenFrame";
                }
            } else {
                request.transport = this.defaultTransport;
            }
        }

        // Verify that the transport is available
        this.checkTransportAvailable(request, (explicitTransport != null));


        //>!BackCompat 2007.2.14 paramsOnly renamed to useSimpleHttp
        if (request.useSimpleHttp == null) request.useSimpleHttp = request.paramsOnly;
        //<!BackCompat


        // default prompt-related settings on request to RPCManager defaults
        // ---------------------------------------------------------------------------------------
        isc.addDefaults(request, {
            showPrompt: this.showPrompt,
            promptStyle: this.promptStyle,
            promptCursor: this.promptCursor,
            useCursorTracker: this.useCursorTracker,
            cursorTrackerConstructor: this.cursorTrackerConstructor
        });

        request.cursorTrackerProperties = isc.addProperties({}, this.cursorTrackerDefaults,
                                                            this.cursorTrackerProperties,
                                                            request.cursorTrackerProperties);

        if (request.cursorTrackerProperties == null)
            request.cursorTrackerProperties = this.cursorTrackerProperties;

        // if request.operation is supplied by callers doing DataSource operations.  If not
        // supplied, add it so we can assume its there for logging, etc.
        if (!request.operation) {
            request.operation = {
                ID: "custom",
                type: "rpc"
            }
        }

        var transformedData = this.transformRequest(request);
        // correct the common error of returning the rpcRequest itself incorrectly, which is
        // never right since the rpcRequest contains various widgets and other data
        // inappropriate to send to the server.
        if (transformedData !== request) {
            request.data = transformedData;
        }

        // support new RPCManager.requestsArePending() API
        this.pendingRpcs++;
        if (this.canQueueRequest(request, (explicitTransport!=null))) {
            if (!this.currentTransaction) this.currentTransaction = this._createTransaction();
            this._addRequestToTransaction(request, this.currentTransaction);
            // if we're not queuing, send it off
            if (!this.queuing) return this.sendQueue();
            return request;

        } else {
            return this.sendNoQueue(request);
        }
    },

    // verify that the specified request.transport is available
    checkTransportAvailable : function (request, isExplicit) {

        var xmlHttpRequestAvailable = this.xmlHttpRequestAvailable();

        var transport = request.transport || this.defaultTransport;
        if (!xmlHttpRequestAvailable) {
            if (transport == "xmlHttpRequest") {
                if (isExplicit) {
                    this.logWarn("RPC/DS request specifically requesting the xmlHttpRequest" +
                                " transport, but xmlHttpRequest not currently available -" +
                                " switching transport to hiddenFrame.");
                } else {
                    this.logWarn("RPCManager.defaultTransport specifies xmlHttpRequest, but"
                             +" xmlHttpRequest not currently available - switching transport "
                             + "to hiddenFrame.");
                }
            }
            request.transport = "hiddenFrame";
        }
    },

    // determine whether a request can be queued
    canQueueRequest : function (request, transportIsExplicit) {
        // since timeouts are controlled on a per-transaction basis, this type of request must be
        // sent separately of any queue
        if (request.ignoreTimeout) request.sendNoQueue = true;

        var transport = request.transport;
        

        // relogin flow: request is attempting login while transactions are suspended - bypass
        // queuing
        if (request.containsCredentials) {
            return false
        }

        // explicitly avoid any existing queue
        // scriptInclude-based requests can't be queued by nature
        if (request.sendNoQueue || request.transport == "scriptInclude") return false;

        // this tracks if we have at least one request on the queue - we can't use this.queuing here
        // because queuing can be disabled while there are still requests on the queue:
        // startQueue(false) - also for the checks below we really only care about requests that go
        // to the server, so we can ignore clientOnlyRequests
        var haveServerRequestsOnQueue = (this.currentTransaction &&
            this.currentTransaction.requestData.operations.length > 0);

        // - current request specifies URL other than that for which we're queueing
        //     - send errant request, continue queueing
        if (haveServerRequestsOnQueue && (request.actionURL != this.currentTransaction.URL)) {
            //>DEBUG
            this.logWarn("RPCRequest specified (or defaulted to) URL: " + request.actionURL
                + " which is different than the URL for which the RPCManager is currently queuing: "
                + this.currentTransaction.URL + " - sending this request to server and continuing to queue");
            //<DEBUG
            return false;
        }

        // Since we parse responses in a single block we can't queue requests expected
        // to issue "strict" JSON responses with those expected to issue standard
        // "eval" type responses.
        if (haveServerRequestsOnQueue &&
            (!!request.useStrictJSON != !!this.currentTransaction.useStrictJSON))
        {
            this.logWarn("RPCManager - attempt to queue request specified 'useStrictJSON:" +
                        request.useStrictJSON + ". This conflicts with this setting for " +
                        "other queued requests - sending the request to server and " +
                        "continuing to queue.");
            return false;
        }

        // - multiop with mixed xmlHttp/frames transports
        //      - send the offending current request, continue queuing
        if (haveServerRequestsOnQueue &&
            (this.currentTransaction.transport != request.transport))
        {
            //>DEBUG
            this.logWarn("RPCRequest with conflicting transport while queuing, sending request to"
                + " server and continuing to queue.");
            //<DEBUG
            return false;
        }

        // can queue
        return true;
    },

    // send a request immediately, bypassing the current queue.
    sendNoQueue : function (request) {
        var currentTransaction = this.currentTransaction;
        var queuing = this.queuing;
        this.currentTransaction = this._createTransaction();
        this._addRequestToTransaction(request, this.currentTransaction);
        var sendResult = this.sendQueue();
        this.queuing = queuing;
        this.currentTransaction = currentTransaction;
        return sendResult;
    },

    _createTransaction : function () {
        // create a new transaction
        var transactionNum = this._nextTransactionNum++;

          var transaction = {
            timeout: this.defaultTimeout,
            transactionNum:transactionNum,
            // the request data and context for all operations in the transaction
            operations:[],
            responses:[],
            // the actual data to go to the server
            requestData:{transactionNum:transactionNum, operations:[]},
            // prompt to show
            prompt: this.defaultPrompt,
            showPrompt: false,
            changed : function () {
                isc.RPCManager._transactions.setLastChanged(this);
                isc.RPCManager._transactions.dataChanged();
                isc.RPCManager._transactions.clearLastChanged();
            }
        }
        // explicitly notify RPCTracker that this is the changed transaction
        this._transactions.addTrack(transaction);
        this._transactions.clearLastChanged();

        return transaction;
    },

    _addRequestToTransaction : function (request, transaction) {
        transaction.URL = request.actionURL;

        // mark transaction as a loginRPC if the request specifies it so we can quickly
        // determine this later (w/o scanning requests)
        if (request.containsCredentials) transaction.containsCredentials = true;

        if(!transaction.download_filename) transaction.download_filename = request.download_filename;

        if (((request.downloadResult || request.downloadToNewWindow) && request.download_filename )
                || request._returnStreamFileURL)
        {
            transaction.download_filename = request.download_filename;
            if (request.download_filename) {
                transaction.URL = this.addPathToURL(transaction.URL,
                                      "/" + encodeURIComponent(request.download_filename));
            }
            // hitting 'cancel' on a download box can cause the server to stall and eventually time
            // out - ignore this error case
            transaction.ignoreError = true;
        } else if (request.exportFilename) {
            // for operations that result in downloaded file, make the browser give the
            // downloaded file a particular default name
            transaction.URL = this.addPathToURL(transaction.URL, "/" + encodeURIComponent(request.exportFilename));
        }


        // grab the prompt of the first operation to define one
        if (request.prompt && !transaction.customPromptIsSet) {
            //>DEBUG
            this.logDebug("Grabbed prompt from first request that defined one: " + request.prompt);
            //<DEBUG
            transaction.prompt = request.prompt;
            transaction.customPromptIsSet = true;
        }



        // set the showPrompt for the transaction and mark the request that forced the prompt so we
        // can hide it once we're done processing that request
        if (request.showPrompt && !transaction.showPrompt && !this.neverShowPrompt) {
            request.showedPrompt = true;
            isc.addProperties(transaction, {
                showPrompt: true,
                promptStyle: request.promptStyle,
                promptCursor: request.promptCursor,
                useCursorTracker: request.useCursorTracker,
                cursorTrackerConstructor: request.cursorTrackerConstructor,
                cursorTrackerProperties: request.cursorTrackerProperties
            });
        }

        if (request.isProxied) {
            isc.addProperties(transaction, {
                isProxied: true,
                proxiedURL: request.proxiedURL
            });
        }

        transaction.transport = request.transport;
        transaction.useStrictJSON = request.useStrictJSON;

        // if any request in a transaction specifies ignoreReloginMarkers, then it applies to
        // the whole transaction since relogin marker processing happens on the whole
        // transaction response.
        if (request.ignoreReloginMarkers) transaction.ignoreReloginMarkers = true;

        transaction.operations.add(request);

        // add request.data to list of operations.
        // XXX null and empty strings become <elem></elem> on the server which means they can't be
        // disambiguated from one another.  Also <elem></elem> doesn't translate to an entry of any
        // kind in the operations list on the server (e.g. a list with just <elem></elem> is a zero
        // length list).  This means we have to encode these two values to reliably pick them up on
        // the server.
        var data = request.data;
        if (data == null) data = "__ISC_NULL__";
        else if (data === "") data = "__ISC_EMPTY_STRING__";

        // if this is a clientOnly request, no data will be sent to the server for it
        if (!request.clientOnly) transaction.requestData.operations.add(data);

        // omit nulls must be on for all requests in transaction for us to enable the flag
        if (transaction.omitNullMapValuesInResponse !== false && request.omitNullMapValuesInResponse != null) {
            transaction.omitNullMapValuesInResponse =
                transaction.requestData.omitNullMapValuesInResponse = request.omitNullMapValuesInResponse;
        } else {
           transaction.omitNullMapValuesInResponse = false;
        }

        if (request.ignoreTimeout) transaction._clearOnTimeout = true;

        request.transactionNum = transaction.transactionNum;

        // XXX expand this?
        if (request.timeout || request.timeout === 0) transaction.timeout = request.timeout;

        transaction.changed();
    },

    //> @classMethod RPCManager.startQueue()
    //
    // Start queuing requests.  When queuing requests, an HTTP request will not be sent to
    // the server until RPCManager.sendQueue() is called.
    // <p>
    // All requests in a given queue must go to the same actionURL and use the same transport
    // (XMLHttp or frames).  If a request specifies a different actionURL or transport than
    // that of the requests currently on the queue, it will be sent to the server separately,
    // ahead of the queue, and a warning will be logged to the Developer Console.
    // <p>
    // Note that the server must process all requests sent as part of the queue before any
    // response is sent to the client.  You should avoid batching a request that will take a
    // long time to process on the server with any other requests because it will delay the
    // response of all requests in the queue.
    //
    // @param [shouldQueue] (boolean) whether queuing should be enabled, default true.  Passing false
    //                      will disable queuing but not send the queue yet, so that any
    //                      queued requests will be sent along with the next
    //                      send()/sendRequest()
    //
    // @return    (boolean)    whether queuing was already enabled before we called
    //
    // @see RPCManager.sendQueue()
    //
    // @requiresModules SCServer
    // @visibility external
    //<
    startQueue : function (shouldQueue) {
        var wasAlreadyQueuing = this.queuing;
        this.queuing = (shouldQueue == null ? true : shouldQueue);
        return wasAlreadyQueuing;
    },

    _promptCounter: 0,
    doShowPrompt : function (transaction, prompt, promptDelay) {
        if (this._promptCounter++ != 0) return;



        if (promptDelay && !this._showedMask) {
            // When the prompt, dialog or cursor, is delayed we show the click mask
            // immediately to prevent any user interaction before the prompt is shown.
            // Note: EH scribbles on the unmaskedTargets array, so duplicate
            isc.EH.showClickMask(null, "hard", this.unmaskedTargets ? this.unmaskedTargets.duplicate() : null, "blockingRPC");
            this._showedMask = true;
        }

        var _this = this;
        var showPrompt = function () {
            if (transaction.promptStyle == "dialog" && prompt != null) {
                isc.showPrompt(prompt);
                _this._showedPrompt = true;
                // If we showed a click mask before the dialog was shown we can
                // remove it now. The modal dialog shows a mask as well - no
                // need to keep two masks in place.
                if (_this._showedMask) {
                    isc.EH.hideClickMask("blockingRPC");
                    _this._showedMask = null;
                }
            } else {
                // cursor
                if (!_this._showedMask) {
                    // Note: EH scribbles on the unmaskedTargets array, so duplicate
                    isc.EH.showClickMask(null, "hard", _this.unmaskedTargets ? _this.unmaskedTargets.duplicate() : null, "blockingRPC");
                    _this._showedMask = true;
                }
                if (transaction.useCursorTracker) {
                    _this._cursorTracker = isc.ClassFactory.getClass(transaction.cursorTrackerConstructor)
                        .create(transaction.cursorTrackerProperties);
                    _this._cursorTracker.show();
                } else {
                    isc.EH._screenSpan.setCursor(transaction.promptCursor);
                }
            }
        };

        if (promptDelay) {
            this._delayedShowPromptTimer = isc.Timer.setTimeout(showPrompt, promptDelay);
        } else {
            showPrompt();
        }
    },

    // cursors: http://www.gtalbot.org/DHTMLSection/Cursors.html
    doClearPrompt : function (transaction) {
        // Cancel delay timer
        if (this._delayedShowPromptTimer) {
            isc.Timer.clear(this._delayedShowPromptTimer);
            this._delayedShowPromptTimer = null;
        }

        if (transaction.clearedPrompt) return;
        transaction.clearedPrompt = true;

        if (--this._promptCounter != 0) {
            // safety net - whouldn't need to reset this, but we check for zero explicitly in
            // doShowPrompt
            if (this._promptCounter < 0 ) this._promptCounter = 0;
            return;
        }

        if (this._showedPrompt) {
            isc.clearPrompt();
        } else {
            if (this._cursorTracker) {
                this._cursorTracker.destroy();
                this._cursorTracker = null;
            } else {
                if (isc.EH._screenSpan) isc.EH._screenSpan.setCursor(isc.Canvas.DEFAULT);
            }
        }
        this._showedPrompt = null;

        if (this._showedMask) {
            isc.EH.hideClickMask("blockingRPC");
            this._showedMask = null;
        }
    },

    //> @classMethod RPCManager.getCurrentTransactionId()
    // Returns the id of the current transaction (a queue of requests).
    // <P>
    // This method must be called after startQueue() has been called and
    // at least one request has been issued.
    //
    // @return (int) the transactionNum of the current transaction.
    // @visibility external
    //<
    getCurrentTransactionId : function () {
        return this.currentTransaction ? this.currentTransaction.transactionNum : null;
    },

    //> @classMethod RPCManager.cancelQueue()
    // Cancel a queue of requests (also called a transaction).
    // <P>
    // If a transactionId is passed, that transaction will be cancelled, otherwise, the current
    // (not yet sent) transaction is cancelled.  You can retrieve the id of the current
    // transaction, if there is one, by calling
    // +link{RPCManager.getCurrentTransactionId(), getCurrentTransactionId()} before the
    // transaction has been sent.
    // <P>
    // Note that cancelQueue() calls +link{RPCManager.clearTransaction(), clearTransaction()}
    // and attempts to abort the request.  However, note also that whilst cancelling a
    // transaction that has already been sent will not necessarily stop the HTTP request that
    // has been issued - this is only possible on some browsers and with some transports - it
    // will reliably cause SmartClient to ignore any response returned by the server and not
    // fire any callbacks that have been passed in.
    //
    // @param [transactionNum] (int) transactionId of the queue.
    // @visibility external
    //<
    cancelQueue : function (transactionNum) {
        if (transactionNum == null) {
            // cancel the current transaction
            transactionNum = this.currentTransaction;
        }
        var transaction = this.getTransaction(transactionNum);
        if (transaction == null) return;
        else if (transaction == this.currentTransaction) this.currentTransaction = null; // clear if current

        // clear the prompt if any of our operations showed it.
        if (transaction.showPrompt) this.doClearPrompt(transaction);

        // If an abort function is present on the transportRequest object
        // returned from the transport function, fire it to kill the transport
        // Note that this is not guaranteed to be present - depends on the implementation of
        // the transport function called by sendQueue
        if (transaction.transportRequest && transaction.transportRequest.abort) {
            transaction.transportRequest.abort();
        }
        transaction.cancelled = true;

        this.clearTransaction(transaction.transactionNum);
    },

    // Returns the transaction object for a transasctionNum.  If passed a transaction object,
    // returns that.  Can also take a native window object representing a HiddenFrame and
    // return the transaction occurring in that frame.
    getTransaction : function (transaction) {
        if (transaction == null) return null;

        // window object
        if (transaction.location && transaction.document) {
            var win = transaction;
            // no transactionNum in the URL - this is probably because the server send an
            // HTTP redirect to a login URL instead of just feeding the login page contents
            // in response to our RPC.
            var hiddenFrames = isc.HiddenFrame._hiddenFrames;
            for (var i = 0; i < hiddenFrames.length; i++) {
                if (win == hiddenFrames[i].getHandle()) {
                    transaction = hiddenFrames[i].transactionNum;
                    break;
                }
            }
            // still haven't found it
            if (transaction == win) {
                this.logDebug("Can't find transactionNum in getTransaction from iframe");
                return null;
            }
        }

        // transaction id passed
        //
        // NOTE: this has to be last, because the code above that finds the transaction via the
        // window object depends on leaving transaction as a Number when it's done
        if (isc.isA.Number(transaction) || isc.isA.String(transaction)) {
            transaction = this._transactions.find({transactionNum: transaction});
        }

        // NOTE: when trackRPCs mode is activated in the Developer Console, we retain
        // transactions that have been cleared.  But these should not be returned.
        if (transaction && transaction.cleared) return null;

        return transaction;
    },

    getCurrentTransaction : function () {
        return this.currentTransaction;
    },

    getLastSubmittedTransaction : function () {
        return this._transactions[this._transactions.length-1];
    },

    //> @classMethod RPCManager.clearTransaction() [A]
    // Erase all client-side record of a transaction, such that any response from the server
    // will be ignored.
    // <P>
    // A transaction means a batch of one or more RPCRequests that have already been sent to
    // the server via +link{RPCManager.sendQueue()}.
    // <P>
    // You can retrieve the id of the current transaction, if there is one, by
    // +link{RPCManager.getCurrentTransactionId(), getCurrentTransactionId()} before the
    // transaction is sent.
    //
    // @param transactionNum (int) id of the transaction to be cleared
    // @see group:relogin
    // @visibility external
    //<
    clearTransaction : function (transactionNum) {
        var transaction = this.getTransaction(transactionNum);
        if (transaction == null) {
            this.logWarn("clearTransaction: no such transaction: " + this.echo(transactionNum));
            return;
        }
        this.clearTransactionTimeout(transaction);
        

        // Don't remove RPCs from the transaction queue until we've had a chance to read the
        // log cookie that tells us whether the user wants to track them or not
        //
        // Have to wait until page load to read cookies
        if (!this._initializedTrackRPC && isc.Page.isLoaded()) {
            var globalLogCookie = isc.LogViewer.getGlobalLogCookie();
            this.setTrackRPC(globalLogCookie ? globalLogCookie.trackRPC : false);
        }

        // flag transaction as cleared, if not tracking, remove from transactions queue
        transaction.cleared = true;
        if (!this._trackRPC) this._transactions.remove(transaction);
        else transaction.changed();

        isc.RPCManager._activeTransactions.remove(transaction.transactionNum);
    },

    // internal method to flip the _trackRPC bit.  If tracking is disabled, clear any
    // transactions from the transactions queue that have completed (were marked as cleared)
    setTrackRPC : function (track) {
        this._trackRPC = track;
        if (!track) this.removeClearedRPC();
    },

    removeClearedRPC : function () {
        var cleared = this._transactions.findAll("cleared", true);
        if (cleared) this._transactions.removeList(cleared);
    },

    // Usually called by server to block new transactions and delay outstanding ones from timing out
    // while waiting for the user to do something - e.g. complete reauthentication.

    //> @classMethod RPCManager.delayAllPendingTransactions()
    //
    // Clears the timeouts for all currently pending RPCRequests/DSRequests and forces any
    // future RPCRequests/DSRequests into an internal queue that doesn't go to the server until
    // you call +link{RPCManager.resendTransaction()}.
    //
    // @see RPCManager.resendTransaction()
    //<
    delayAllPendingTransactions : function () {

        // set a global flag so any newly created transactions don't get sent to the server
        this.delayingTransactions = true;

        // clear the timeouts for outstanding transactions
        for (var i = 0; i < this._transactions.length; i++) {
            var transaction = this._transactions[i];
            this.delayTransaction(transaction);
        }
    },

    //> @classMethod RPCManager.suspendTransaction() [A]
    // Suspends the current transaction, such that all processing of the transaction is halted,
    // any remaining +link{rpcRequest.callback,callbacks} in the transaction won't fire, and
    // the transaction can never +link{rpcRequest.timeout,timeout}.
    // <P>
    // <code>suspendTransaction()</code> is typically used to handle total failures for an
    // entire transaction, such as HTTP status 500, or session timeout resulting in
    // +link{RPCManager.loginRequired,loginRequired()} being called.  In both cases the intent
    // is to put the transaction on hold so that a transient problem can be resolved, and then
    // the transaction can be re-sent successfully.  By using suspendTransaction(), components
    // that submitted requests never realize there was a transient failure, and so error
    // handling logic does not have to be implemented in every component.
    // <P>
    // Generally you can only validly suspend a transaction from either
    // +link{RPCManager.loginRequired()} or +link{RPCManager.handleError()}, and in the case of
    // handleError(), only when the first response in the transaction has an error.  Suspending
    // and re-sending a partially processed transaction means that some responses will be
    // processed twice, with undefined results for requests issued automatically by UI
    // components.
    // <P>
    // A suspended transaction must ultimately be either cleared via
    // +link{clearTransaction()} or re-sent via +link{resendTransaction()} or memory will be
    // leaked.
    //
    // @param [transaction] (transaction Obj or ID) transaction to delay.  Defaults to the
    //                      current transaction if there is one
    // @see RPCManager.resendTransaction()
    // @visibility external
    //<

    suspendTransaction : function (transactionNum) {
        var transaction = this.getTransaction(transactionNum) ||
                            this.getCurrentTransaction();
        if (transaction == null) {
            this.logWarn("No transaction to suspend");
            return;
        }
        if (transaction.suspended) return;

        transaction.suspended = true;

        // If we're currently in a thread kicked off from a transaction response, set
        // an additional flag to suppress subsequent callbacks in the same thread, even if
        // the transaction is no longer marked as suspended (has been re-submitted)
        if (transaction._handlingResponse) transaction.abortCallbacks = true;

        this.clearTransactionTimeout(transaction);
        if (transaction.showPrompt) this.doClearPrompt(transaction);

        transaction.changed();
    },

    // this is called by the internal RPC page load defer mechanism
    delayTransaction : function (transaction) {
        transaction = this.getTransaction(transaction);
        if (transaction.delayed) return;
        transaction.delayed = true;
        this.clearTransactionTimeout(transaction);

        transaction.changed();
    },


    //>Offline
    onLine: !isc.isOffline(),

    goOffline : function () {
        this.logInfo("Going offline...");
        this.onLine = false;
    },
    goOnline : function () {
        this.logInfo("Going online...");
        // replay transactions
        this.offlinePlayback = true;
        this.playbackNextOfflineTransaction();
    },

    // observable
    offlineTransactionPlaybackComplete : function () { },

    playbackNextOfflineTransaction : function () {
        var transaction = this.offlineTransactionLog ? this.offlineTransactionLog.removeAt(0) : null;
        if (transaction == null) {
            this.logInfo("Offline transaction playback complete");
            // no more to playback, go online if the browser is online
            this.offlinePlayback = false;
            this.onLine = !isc.isOffline();
            this.offlineTransactionPlaybackComplete();
            return;
        }

        // play it back
        // We'll get called back by performTransactionReply()
        this.resubmitTransaction(transaction);
    },

    offlineTransaction : function (transaction) {
        if (transaction.offline) return;
        transaction = this.getTransaction(transaction);
        transaction.offline = true;

        this.clearTransactionTimeout(transaction);

        if (!this.offlineTransactionLog) {
            this.offlineTransactionLog = [];
            // keep it sorted by timestamp.  this is important to keep transactions that are
            // added via transactionTimeout(0 in order
            this.offlineTransactionLog.sortByProperty("timestamp", Array.ASCENDING);
        }
        this.offlineTransactionLog.add(transaction);

        transaction.changed();

        // fire fake reply callbacks
        var requests = transaction.operations;
        for (var i = 0; i < requests.length; i++) {
            var request = requests[i];
            var response = this.createRPCResponse(transaction, request, {
                httpResponseCode: 200,
                offlineResponse: true
            });

            this.delayCall("fireReplyCallbacks", [request, response], 0);
        }
    },
    //<Offline

    // if we're delaying transactions, this resubmits outstanding transactions and allows new ones
    // to go to the server.

    //> @classMethod RPCManager.resendTransaction() [A]
    // Resend a suspended transaction to the server.  See +link{suspendTransaction()} for
    // context.
    // <P>
    // Note that the transaction must have been previously suspended, and in particular
    // suspended validly according to the rules described in the docs for
    // +link{suspendTransaction()}, or undefined results will occur.
    // <P>
    // You can resend <b>all</b> suspended transactions by calling
    // +link{resendTransaction()} with no arguments.
    //
    // @param [transactionNum] (int) id of the transaction to be re-sent, or null to resend all
    //                              suspended transactions
    // @see group:relogin
    // @visibility external
    //<
    resendTransaction : function (transaction) {
        this.resendTransactionsFlagged(transaction, "suspended");
    },

    // this is called by the internal RPC page load defer mechanism
    resendDelayedTransactions : function () {
        this.delayingTransactions = false;
        this.resendTransactionsFlagged(null, "delayed");
    },
    resendTransactionsFlagged : function (transaction, flag) {
        // resend either just the transaction passed in or all intercepted transactions (no
        // args)
        var transactions = transaction ? [this.getTransaction(transaction)] : this._transactions;

        for (var i = 0; i < transactions.length; i++) {
            transaction = transactions[i];
            if (transaction[flag]) {
                delete transaction[flag];
                this.resubmitTransaction(transaction);
            }
        }
    },

    // returns all rpcRequests for a given operation
    //
    // This may be needed for some very advanced cases of relogin or other queuing and
    // cancelling logic.
    getTransactionRequests : function (transaction) {
        return this.getTransaction(transaction).operations;
    },

    _setTransactionTimeoutTimer : function (transaction) {
        transaction = this.getTransaction(transaction);

        var timeout = transaction.timeout;
        if(!timeout && timeout !== 0) timeout = this.defaultTimeout;
        if(timeout == 0) return;

        transaction.timeoutTimer =
            isc.Timer.setTimeout("isc.RPCManager._timeoutTransaction("
                                 + transaction.transactionNum + ")", timeout);
    },

    clearTransactionTimeout : function (transaction) {
        transaction = this.getTransaction(transaction) ||
                      this.getCurrentTransaction() ||
                      this.getLastSubmittedTransaction();
        if(!transaction) return;

        isc.Timer.clear(transaction.timeoutTimer);
    },

    _timeoutTransaction : function (transaction) {
        transaction = this.getTransaction(transaction);

        if (transaction._clearOnTimeout) {
            
            this.clearTransaction(transaction);
            return;
        }

        //>Offline
        if (!this.onLine) {
            // we went offline after the transaction was sent - consider this transaction
            // offline instead of returning an error
            this.offlineTransaction(transaction);
            return;
        }
        //<Offline

        transaction.results = this._makeErrorResults(transaction, {
             data: isc.RPCManager.timeoutErrorMessage,
             status: isc.RPCResponse.STATUS_SERVER_TIMEOUT
        });
        this._performTransactionReply(transaction.transactionNum)
    },

    _makeErrorResults : function (transaction, result) {
        var results = [];
        for(var i = 0; i < transaction.operations.length; i++) {
            if (transaction.operations[i].dataFormat == "xml") {
                results[i] = isc.Comm.xmlSerialize("response", result);
            } else {
                results[i] = {response:isc.clone(result)};
            }
        }
        return results;
    },

    // resubmit a transaction to the server.  Used to proceed after a transient or temporary
    // error, like being unable to contact the server or needing to authenticate again because
    // your session timed out.
    resubmitTransaction : function (transaction) {
        transaction = this.getTransaction(transaction) ||
                      this.getLastSubmittedTransaction();

        // wipe out stored status (eg transport error)
        transaction.status = null;

        // could be queuing a new transaction - save it off
        var queuingTransaction = this.currentTransaction;

        // sendQueue() operates on this.currentTransaction, so set it to the resubmitTransaction
        this.currentTransaction = transaction;

        if (transaction != null) {
            //>DEBUG
            this.logInfo("Resubmitting transaction number: " + transaction.transactionNum);
            //<DEBUG

            // if there is an operation window open from the transaction being
            // resubmitted, get rid of it
            


            delete transaction.suspended;
            delete transaction.clearedPrompt;

            // if this transaction was deferred because it was sent before page load,
            // re-instate the arguments originally passed to sendQueue() if any
            var args = transaction._args || isc.emptyObject;
            this.sendQueue(args.callback, args.prompt, args.URL);
        //>DEBUG
        } else {
            this.logWarn("No transaction to resubmit: transaction number "
                         + transaction + " does not exist");
        //<DEBUG
        }
        // reinstate the original currentTransaction
        this.currentTransaction = queuingTransaction;
    },

    // called by the SmartClient server to work around an IE issue specific to a single service
    // pack, where the browser occasionally submits an empty form
    retryOperation : function (commFrameID) {
        this.logDebug("Server-initiated operation retry for commFrameID: " + commFrameID);
        var commFrame = window[commFrameID];
        if(!commFrame) {
            this.logError("comm operation retry failed - can't locate object: " + commFrameID);
            return;
        }
        commFrame.sendData(true);
    },

    transactionAsGetRequest : function (transaction, baseURL, params) {
        if (!transaction.cleared) transaction = this.getTransaction(transaction) || this.getCurrentTransaction();
        baseURL = (baseURL || transaction.URL || this.getActionURL());
        if(!params) params = {};
        params._transaction = this.serializeTransaction(transaction);

        return this.addParamsToURL(this.markURLAsRPC(baseURL), params);
    },

    // encode a parameter for use in a URL query string or form-style HTTP POST.
    // What to do with non-String param values:
    // While Strings, Numbers, and Booleans can be sent just as an HTML form sends
    // them, we're not in a context here to know what the server expects for Dates or
    // other types - if a particular caller wants a specific type of serialization that
    // caller needs to serialize the data before it gets to this layer.
    // NOTE: Dates: JSON.encode() would currently return "new Date(.." for a Date;
    // using the XML Schema format is a better default.  Note also that we support a flag
    // on Dates "logicalDate" that causes it to be serialized showing just the date with no
    // time values, or "logicalTime" to serialize just the time value (not the date).
    encodeParameter : function (paramName, paramValue) {
        if (isc.isA.Date(paramValue)) {
            isc.Comm.xmlSchemaMode = true;
            paramValue = paramValue.toSchemaDate();
            isc.Comm.xmlSchemaMode = null;
        } else if (isc.isA.Array(paramValue)) {
            // for Array values, list the same parameter multiple times.  This
            // matches what HTML forms do for multiple selects
            var output = isc.SB.create();
            for (var i = 0; i < paramValue.length; i++) {
               output.append(this.encodeParameter(paramName, paramValue[i]));
               if (i < paramValue.length-1) output.append("&");
            }
            return output.toString();
        } if (!isc.isA.String(paramValue)) {
            paramValue = isc.JSON.encode(paramValue, {prettyPrint:false});
        }
        return isc.SB.concat(encodeURIComponent(paramName), "=",
                             encodeURIComponent(paramValue));
    },

    addParamsToURL : function (baseURL, params) {
        var result = baseURL;
        if (!params) return baseURL;
        for (var paramName in params) {
            var paramValue = params[paramName];

            result += result.contains("?") ? "&" : "?";
            result += this.encodeParameter(paramName, paramValue);
        }
        return result;
    },

    addPathToURL : function (baseURL, path) {
        var newURL;

        var splitIndex = baseURL.indexOf("?");
        if (splitIndex == -1) {
            newURL = baseURL+path;
        } else {
            newURL = baseURL.substring(0, splitIndex)+path+baseURL.substring(splitIndex);
        }

        return newURL;
    },

    // based on the dataEncoding setting returns the string-serialized version of the
    // requestData member of the passed in transaction object.
    serializeTransaction : function (transaction) {
        var result;

        // If we're in SGWT the 'data' for the transaction may include native
        // Java objects assigned by a call to JSOHelper.setAttribute or
        // JSOHelper.setAttributeAsJavaObject.
        // We have no way to serialize such an object for transmission to the server,
        // so throw an exception in the serialize logic if such an object is
        // encountered.

        if (isc.Browser.isSGWT) {
            window.SmartGWT.warnOnSerializeError = true;
            window.SmartGWT.serializeErrorMessage =
                "Object is part of a request being serialized for " +
                "transmission to the server. See SmartClient documentation of " +
                "RPCRequest.data for a table of Java types that can be converted automatically.";
        }
        if (this.dataEncoding == "JS") {

            isc.Comm._legacyJSMode = true;
            if (isc.isAn.Array(transaction.operations)) {
                var ds = isc.DataSource.get(transaction.operations[0].dataSource);
                if (ds) {
                    isc.Comm._trimMillis = !!ds.trimMilliseconds;
                }
            }
            result = isc.Comm.serialize(transaction.requestData);
            isc.Comm._trimMillis = null;
            isc.Comm._legacyJSMode = null;
        } else {
            isc.Comm._explicitNils = true;
            if (isc.isAn.Array(transaction.operations)) {
                // NOTE: this implies that you can only have one trimMilliseconds setting per
                // queue - but that's OK because we only anticipate the need for different
                // millisecond settings if you are going to different servers
                var ds = isc.DataSource.get(transaction.operations[0].dataSource);
                if (ds) {
                    isc.Comm._trimMillis = !!ds.trimMilliseconds;
                }
            }
            result = isc.Comm.xmlSerialize("transaction", transaction.requestData);
            isc.Comm._trimMillis = null;
            isc.Comm._explicitNils = null;
        }
        if (isc.Browser.isSGWT) {
            window.SmartGWT.warnOnSerializeError = false;
            window.SmartGWT.serializeErrorMessage = null;
        }
        //this.logWarn("serialized transaction: " + result);
        return result;
    },


    markURLAsRPC : function (URL) {
        if(!URL.contains("isc_rpc=")) URL += (URL.contains("?") ? "&" : "?")
                                                    + "isc_rpc=1&isc_v="+isc.versionNumber;
        return URL;
    },

    markURLAsXmlHttp : function (URL) {
        if(!URL.contains("isc_xhr=")) URL += (URL.contains("?") ? "&" : "?")  + "isc_xhr=1";
        return URL;
    },

    addDocumentDomain : function (URL) {
        if(!URL.contains("isc_dd=")) URL += (URL.contains("?") ? "&" : "?")  + "isc_dd="+document.domain;
        return URL;
    },

    registerUnmaskedTarget : function (unmaskedTarget) {
        if (!this.unmaskedTargets) this.unmaskedTargets = [];
        this.unmaskedTargets.add(unmaskedTarget);
    },
    unregisterUnmaskedTarget : function (unmaskedTarget) {
        if (this.unmaskedTargets) this.unmaskedTargets.remove(unmaskedTarget);
    },

    //> @classMethod RPCManager.sendQueue()
    //
    // Send all currently queued requests to the server.  You need only call this method if you are
    // using queuing otherwise your requests are synchronously submitted to the server.
    // <br><br>
    // NOTE: if you aren't the caller who first enables queuing (startQueue() returns
    // true), you should in general avoid calling sendQueue(), because whoever was
    // first to enable queuing may have more requests to add to the same queue.
    //
    // @param [callback] (RPCQueueCallback) Callback to fire when the queued operations complete. Callback
    // will be fired with 1 parameter: <code>responses</code> an array of +link{DSResponse} or
    // +link{RPCResponse} objects that were part of the transaction fired by this method.
    //
    // @see classMethod:RPCManager.send()
    // @see classMethod:RPCManager.sendRequest()
    // @see classMethod:RPCManager.startQueue()
    //
    // @requiresModules SCServer
    // @visibility external
    //<
    //> @method Callbacks.RPCQueueCallback
    // Callback to fire when a queue of requests sent via
    // {@link com.smartgwt.client.rpc.RPCManager#sendQueue(RPCQueueCallback)} returns.
    // <P>Note that the Array of RPCResponses passed to this callback
    // will actually be DSResponse objects for any requests that were actually
    // DSRequests.
    // <smartgwt>DSResponse is a subclass of RPCResponse, and you can "typecast" the
    // underlying JavaScript object to a DSResponse like so:<br>
    // <code>new DSResponse(rpcResponse.getJsObj());</code></smartgwt>
    //
    // @param response (Array of RPCResponse) array of responses returned from the sent queue of requests
    //
    // @visibility external
    //<

    sendQueue : function (callback, prompt, URL, delay) {
        var transaction = this.currentTransaction;

        // we're going to submit this transaction or error out in some way - in either way we're not
        // going to continue queueing
        this.currentTransaction = null;
        this.queuing = false;

        if (!transaction) {
            //>DEBUG Note this can happen easily if rpcRequests have been deferred because they
            // are attempted before page load.
            this.logInfo("sendQueue called with no current queue, ignoring");
            //<DEBUG
            return false;
        }
        if (delay) this.delayCall("_sendQueue", [callback,prompt,URL,transaction]);
        else return this._sendQueue(callback,prompt,URL,transaction);
    },

    _sendQueue : function (callback,prompt,URL,transaction) {
        // for flags such as "directSubmit" that affect the entire transaction, use the first
        // request
        var request = transaction.operations[0];

        if ((!isc.Page.isLoaded() || this.delayingTransactions) && !request._returnStreamFileURL) {
            transaction._args = {
                callback: callback,
                prompt: prompt,
                URL: URL
            };
            if (!this.delayingTransactions) {
                isc.Page.setEvent("load", this, isc.Page.FIRE_ONCE, "resendDelayedTransactions");
                this.delayingTransactions = true;
            }
            this.delayTransaction(transaction);
            return request;
        }

        //>Offline
        // keep a timestamp of when the transaction was sent for offline mode
        transaction.timestamp = new Date().getTime();
        if (!this.onLine && !this.offlinePlayback) {
            // check internal onLine flag instead of isc.isOffline() because we want to make
            // sure that offline transaction playback completes before new requests are sent to
            // the server, preserving order
            this.offlineTransaction(transaction);
            return request;
        }
        //<Offline

        // if all the operations are clientOnly, we don't need to go to the server at all
        var allClientOnly = true;
        for (var i = 0; i < transaction.operations.length; i++) {
            if (!transaction.operations[i].clientOnly) {
                allClientOnly = false;
                break;
            }
        }
        if (allClientOnly) {
            transaction.allClientOnly = true;
            transaction.sendTime = isc.timeStamp();
            if (callback != null) {
                transaction._userCallback = callback;
            }

            this.delayCall("_performTransactionReply", [transaction.transactionNum], 0);
            return request;
        }

        // figure out the prompt and URL, saving them on the transaction object in case we need to
        // delay
        // NOTE use Page.getURL() to support special directories such as "[APPFILES]"
        URL = transaction.URL = isc.Page.getURL(URL || transaction.URL || this.getActionURL());
        var addTNumToParams = false;
        if (!request.useSimpleHttp && transaction.transport != "scriptInclude") {
            URL = this.markURLAsRPC(URL);
            // Add transport / transactionNum to URL

            if (transaction.transport == "xmlHttpRequest") {
                URL = this.markURLAsXmlHttp(URL);
                addTNumToParams = true;
            } else {
            URL = this.addParamsToURL(URL, {isc_tnum: transaction.transactionNum});
        }
            if (document.domain != location.hostname) URL = this.addDocumentDomain(URL);
        }
        prompt = transaction.prompt = ((transaction.showPrompt == null || transaction.showPrompt) ?
            (prompt || transaction.prompt || this.defaultPrompt) : null);

        if (prompt) this.doShowPrompt(transaction, prompt, transaction.promptDelay || this.promptDelay);

        // support RPCRequest.params
        var transactionParams = {};
        var haveParams = false;
        for (var i = 0; i < transaction.operations.length; i++) {
            var rpcRequest = transaction.operations[i];
            var params = rpcRequest.params,
                requestHasParams = params != null;

            if (addTNumToParams) {
                params = params || {};
                params.isc_tnum = transaction.transactionNum;
            }

            var queryParams = rpcRequest.queryParams;
            var origParams = params;

            // undocumented - request.queryParams applied to URL - key collisions
            // result in multivalued param on server (which is in contrast to the way
            // request.params works, where last key overrides)
            if (queryParams && isc.isAn.Object(queryParams)) {
                URL = transaction.URL = this.addParamsToURL(URL, queryParams);
            }

            //>DEBUG
            if (requestHasParams && haveParams) {
                this.logWarn("Multiple RPCRequests with params attribute in one transaction - merging");
            }
            //<DEBUG

            if (params) {
                if (isc.isA.String(params)) {
                    if (window[params]) params = window[params]; // component
                    else if (isc.Canvas.getForm(params)) params = isc.Canvas.getForm(params); // native form
                    else {
                        //>DEBUG
                        this.logWarn("RPCRequest: " + isc.Log.echo(rpcRequest)
                            + " was passed a params value: " + params
                            + " which does not resolve to a component or a native"
                            + " form - request to server will not include these params");
                        //<DEBUG
                        params = null;
                    }
                }
                if (isc.isA.Class(params)) {
                    if (params.getValues) params = params.getValues();
                    //>DEBUG
                    else {
                        this.logWarn("RPCRequest: " + isc.Log.echo(rpcRequest)
                            + " was passed an instance of class " + params.getClassName()
                            + " (or a global ID that resolved to this class)"
                            + " - this class does not support the getValues() method - request to"
                            + " server will not include these params");
                    }
                    //<DEBUG
                }

                if (params && !isc.isAn.Object(params)) {
                    //>DEBUG
                    this.logWarn("params value: " + origParams + " for RPCrequest: "
                        + isc.Log.echo(rpcRequest) + " resolved to non-object: "
                        + isc.Log.echo(params) + " - request to server will not include these params");
                    //<DEBUG
                    params = null;
                }

                if (params) {
                    isc.addProperties(transactionParams, params);
                    haveParams = true;
                }
            }
        }
        //>DEBUG
        if (this.logIsInfoEnabled()) {
            this.logInfo("sendQueue[" + transaction.transactionNum + "]: " +
                         transaction.operations.length + " RPCRequest(s); transport: " +
                         transaction.transport + "; target: " + URL);
        }
        //<DEBUG

        if (isc.DataSource) {
            for (var i = 0; i < transaction.operations.length; i++) {
                var rpcRequest = transaction.operations[i];
                isc.DataSource.recordTimingData(rpcRequest, "performDSOperation", "end");
                isc.DataSource.recordTimingData(rpcRequest, "Marshall and send request", "start");
            }
        }
        // about to send
        transaction.changed();
        // call performTransactionReply when the transaction completes (regardless of transport)
        //if (callback) transaction.callback = callback;
        transaction.callback = "isc.RPCManager.performTransactionReply(transactionNum,results,wd)";
        if (callback) transaction._userCallback = callback;
        
            // send the request to the server via the configured transport
            var params = transactionParams;

            // Call isc.Comm.send<TransportType>(...)
            var transport = transaction.transport,
                transportMethodName = "send" + (transport.substring(0,1).toUpperCase()) + transport.substring(1);

            if (isc.Comm[transportMethodName] == null) {
                this.logWarn("Attempt to send transaction with specified transport '"
                             + transaction.transport + "' failed - unsupported transaction type.");
                return;
            }

            this._setTransactionTimeoutTimer(transaction);


            var requestData;
            var restRequests = []
            for (var i = 0; i < transaction.operations.length; i++) {
                if (transaction.operations[i].isRestRequest) {
                  restRequests.push(transaction.operations[i])
                }
            }
            if (restRequests.length > 1) {
                if (restRequests[0].contentType == this._$XML_MIME_TYPE) {
                    requestData = "<transaction transactionNum=\"" + transaction.transactionNum + "\">";
                    requestData += "<operations>"
                    for (var i = 0; i < restRequests.length; i++) {
                        requestData += restRequests[i].data;
                    }
                    requestData += "</operations>";
                    requestData += "</transaction>";
                } else {  // Not XML, must be JSON
                    requestData = "{ \"transaction\": { \"transactionNum\": " + transaction.transactionNum + ", ";
                    requestData += "\"operations\": [";
                    for (var i = 0; i < restRequests.length; i++) {
                        if (i > 0) requestData += ", ";
                        // data parameter could be cleared during DataSource.sendDSRequest method
                        var requestParams = restRequests[i].data || restRequests[i].params;
                        if (!isc.isA.String(requestParams)) {
                            requestParams = isc.JSON.encode(requestParams);
                        }
                        requestData += requestParams;
                    }
                    requestData += "]}}";
                }
             }
             else if(restRequests.length==1) {
                  requestData = restRequests[0].data;
             }
             else if (request.useSimpleHttp) {
                requestData = request.data;
             }
            // Track the transactions that have been sent but not yet returned

            isc.RPCManager._activeTransactions.add(transaction.transactionNum);

            // Allow the transport method to return a transactionRequest object
            transaction.transactionRequest = isc.Comm[transportMethodName]({
                URL: URL,
                httpMethod: request.httpMethod,
                contentType: request.contentType,
                httpHeaders: request.httpHeaders,
                bypassCache: request.bypassCache,
                data: requestData,
                fields: params,
                target: request.target,
                // valid only for scriptInclude
                callbackParam: request.callbackParam,
                transport: transaction.transport,
                blocking: request.blocking,
                useSimpleHttp:request.useSimpleHttp,
                transactionNum: transaction.transactionNum,
                transaction: transaction
            });

        
        // Invoke user hook
        if (isc.isA.Function(this.queueSent)) this.queueSent(transaction.operations);
        //>DEBUG
        transaction.sendTime = isc.timeStamp();
        //<DEBUG

        if (isc.DataSource) {
            for (var i = 0; i < transaction.operations.length; i++) {
                var rpcRequest = transaction.operations[i];
                isc.DataSource.recordTimingData(rpcRequest, "Marshall and send request", "end");
                isc.DataSource.recordTimingData(rpcRequest, "Client processing", "end");
            }
        }
        return request;
    },

    _$XML_MIME_TYPE: "text/xml",

    // called by the rpcRequest.transport with the server results.
    // - for xmlHttpRequest transport, "results" is an xmlHttpRequest, and it's contents may be
    //   eval'd or delivering directly based on settings like evalResult
    // - for other transports, a JavaScript object is delivered
    performTransactionReply : function (transactionNum, results, wd) {
        // look up this transaction
        var transaction = this.getTransaction(transactionNum);
        if (!transaction) {

            //>DEBUG
            this.logWarn("performTransactionReply: No such transaction " + transactionNum);
            //<DEBUG
            return false;
        }


        delete transaction._handlingResponse;
        delete transaction.abortCallbacks;

        transaction.receiveTime = isc.timeStamp();
        transaction.changed();

        isc.RPCManager._activeTransactions.remove(transactionNum);
        //>DEBUG
        this.logInfo("transaction "+transactionNum + " arrived after " +
                     (transaction.receiveTime - transaction.sendTime) + "ms");
        //<DEBUG

        

        // if there are no results, something catastrophic happened
        if (results == null) {
            //>DEBUG
            this.logFatal("No results for transaction " + transactionNum);
            //<DEBUG
            
            return false;
        }

        // results is the xmlHttpRequest object - the results is the responseText
        if (transaction.transport == "xmlHttpRequest") {
            var xmlHttpRequest = results;

            transaction.xmlHttpRequest = xmlHttpRequest;
            results = xmlHttpRequest.responseText;

            // Crazy FF bug - if the network cable is unplugged, accessing xhr.status throws a
            // chrome exception and stops execution - but accessing other attributes such as
            // responseText works fine
            var status;
            try {
                status = xmlHttpRequest.status;
            } catch (e) {
                this.logWarn("Unable to access XHR.status - network cable unplugged?");
                status = -1;
            }

            // In IE, the status code will sometimes be reported as "1223" when IE has
            // actually received "204: No content".  We've seen this with the local HTTPProxy
            // relaying a 204 response from Amazon S3, and this has been publicly reported in
            // connection with "kupu", try searching for "http status 1223"
            if (status == 1223) status = 204;

            // using XMLHttpRequest against the filesystem results in an HTTP code of zero -
            // very strange, but need to support this as a non-error case.
            //
            // Note that status == 0 can also mean a security violation whereby an XHR request
            // to the origin server receives a 302 redirect to a non-origin server.  IE reports
            // this is status 0, so we check against location.protocol here to disambiguate.
            // Note the "app-resource:" protocol is returned by Adobe AIR when accessing
            // files as local resources
            if (status == 0 &&
                (location.protocol == "file:" || location.protocol == "app-resource:"))
                    status = 200;

            // actually modify the response code so upstream code works without handling these
            // cases
            transaction.httpResponseCode = status;
            transaction.httpResponseText = xmlHttpRequest.responseText;

            // relogin support
            // users can specify the ignoreReloginMarkers flag to have login response code
            // processing be disabled - useful for custom network formats where the relogin
            // marker could appear
            if (status != -1 && !transaction.ignoreReloginMarkers &&
                 this.processLoginStatusText(xmlHttpRequest, transactionNum))
            {
                return;
            }

            // additional end-user hook just in case
            if (status != -1 && this.responseRequiresLogin(xmlHttpRequest, transactionNum)) {
                this.handleLoginRequired(transactionNum);
                return;
            }

            if (status != -1 && this.responseIsRelogin(xmlHttpRequest, transactionNum)) {
                this.handleLoginRequired(transactionNum);
                return;
            }

            // see: http://danweber.blogspot.com/2007/04/ie6-and-error-code-12030.html
            if (status == 12030 && isc.Browser.isIE) {
                this.logWarn("Received HTTP status code 12030, resubmitting request");
                this.resubmitTransaction(transactionNum);
                return;
            }


            var url = transaction.URL;
            var realStatus;
            if (transaction.isProxied) {
                url = transaction.proxiedURL+" (via proxy: " + url +")";


                var headers = this.getHttpHeaders(xmlHttpRequest, transaction);
                var proxyHeader;
                if (headers) {

                    for (var key in headers) {
                        if (key.toLowerCase() == "x-isc-httpproxy-status") {
                            proxyHeader = headers[key];
                            break;
                        }
                    }
                }
                if (proxyHeader && proxyHeader == "-91") realStatus = -91;
                if (proxyHeader && proxyHeader == "-92") realStatus = -92;
            }

            if (realStatus) status = 500;

            // All HTTP 2xx codes indicate success.  Success codes other than 200 OK are
            // somewhat obscure, but are used by Amazon S3 and possibly other REST APIs
            if (status > 299 || status < 200) { //error
                results = this._makeErrorResults(transaction, {
                    data : "Transport error - HTTP code: "+ status
                           +" for URL: " + url
                           + (status == 302 ? " This error is likely the result"
                              + " of a redirect to a server other than the origin"
                              + " server or a redirect loop." : ""),
                    status: realStatus ? realStatus : isc.RPCResponse.STATUS_TRANSPORT_ERROR
                });

                this.logDebug("RPC request to: " + url
                             + " returned with http response code: "
                             + status +". Response text:\n"
                             + xmlHttpRequest.responseText);
                transaction.status = realStatus ? realStatus : isc.RPCResponse.STATUS_TRANSPORT_ERROR;

                // Give the developer an opportunity to intercept any transport errors BEFORE we fire
                // any request callbacks
                // Use case: This would give the developer an opportunity to handle intermittent
                // server errors by suspending and resubmitting the transaction before any callback
                // logic attempts to handle the individual responses.
                // Note that this is fired regardless of individual request.willHandleError settings
                //
                // _handlingResponse flag allows us to catch the case where we suspend and
                // immediately resubmit the transaction
                transaction._handlingResponse = true;
                this.handleTransportError(transactionNum, transaction.status,
                                         transaction.httpResponseCode,
                                         transaction.httpResponseText);
                if (transaction.suspended || transaction.abortCallbacks) {
                    delete transaction.abortCallbacks;
                    delete transaction._handlingResponse
                    return;
                }
                delete transaction._handlingResponse;
            }

            if (transaction.httpResponseText != null &&
               transaction.httpResponseText.contains("parent.isc.RPCManager.handleMaxPostSizeExceeded(window.name);"))
            {
                this._handleMaxPostSizeExceeded(transactionNum);
                return;
            }
        } else if (transaction.transport == "hiddenFrame") {

            // relogin support
            // users can specify the ignoreReloginMarkers flag to have login response code
            // processing be disabled - useful for custom network formats where the relogin
            // marker could appear
            if (!transaction.ignoreReloginMarkers &&
                 this.processLoginStatusText(results, transactionNum))
            {
                return;
            }


        }

        // store the results array in the transaction results
        transaction.results = results;


        this._performTransactionReply(transactionNum);
        return true;
    },

    _handleMaxPostSizeExceeded : function (transactionNum) {
        var transaction = this.getTransaction(transactionNum);

        var results = this._makeErrorResults(transaction, {
            data : "The server did not receive the data that was sent to it.  "
                  + "Please see the documentation for isc.RPCResponse.STATUS_MAX_POST_SIZE_EXCEEDED",
            status: isc.RPCResponse.STATUS_MAX_POST_SIZE_EXCEEDED
        });
        transaction.status = isc.RPCResponse.STATUS_MAX_POST_SIZE_EXCEEDED

        // store the results array in the transaction results
        transaction.results = results;

        this._performTransactionReply(transactionNum);
    },
    handleMaxPostSizeExceeded : function (commFrameID) {
        var commFrame = window[commFrameID];
        if(!commFrame) {
            this.logError("Unable to handle max post size exceeded - can't locate object: " + commFrameID);
            return;
        }
        var transaction = this.getTransaction(commFrame);
        if (!transaction) {
           this.logError("Unable to find transaction for comm frame id: "+ commFrameID);
           return;
        }
        this._handleMaxPostSizeExceeded(transaction.transactionNum);
    },

    responseIsRelogin : function (xmlHttpRequest, transactionNum) {
        var status = xmlHttpRequest.status;

        // When the user session has timed out, an HTTP authenticator will typically respond to
        // a request with a 302 redirect.  When document.domain is set, and the redirect
        // targets what looks to the browser like a non-origin server, browsers behave
        // inconsistently (probably because the XHR spec doesn't deal with this issue).  The
        // problem is that since the content targeted by the XHR request doesn't execute,
        // there's no way for the browser to know that it's safe relative to the document.domain
        // setting of the page.  As a result, browsers simply block access to the content that
        // ultimately comes back via the 302 redirect.
        //
        // This is a problem for us because we rely on our relogin markers to execute or arrive
        // as a string so we can tell that a relogin is required.  The solution is as follows:
        //
        // FF: when the above occurs, FF returns an HTTP status code of 302, which is normally
        // impossible, except for a redirect loop.  If the user sets the special
        // treatRedirectAsRelogin flag, we'll use that as the discriminator.  One problem with
        // this is that if the page has set document.domain and the server returns a 302, even
        // one that targets the origin server, FF will report the status as 302.  This means
        // that relogin in FF with document.domain set is not transparently compatible with
        // other logic on the page that may rely on local redirects.
        //
        // IE: If the request that triggered the redirect contained no body, IE reports a
        // status code of 0.  If there was a body, IE bogusly reports status 200, but the xhr
        // contains an empty responseText and empty response headers, so we use these two
        // discriminators in IE.
        //
        // Safari untested.
        if (document.domain != location.hostname &&
            (
             // FF
             (status == 302 && this.treatRedirectAsRelogin) ||
             // IE - no body sent in request that generated the off-site redirect
             (status == 0) ||
             // IE - body was sent in request that generated the off-site redirect
             (status == 200 && xmlHttpRequest.getAllResponseHeaders() == isc.emptyString &&
              xmlHttpRequest.responseText == isc.emptyString)
            ))
        {
            this.logDebug("Detected document.domain 302 relogin condition - status: "+status);
            return true;
        }
        return false;
    },

    //> @classAttr RPCManager.loginStatusCodeMarker (String : RPCManager.loginStatusCodeMarker : IRWA)
    // String sequence which marks the response as a one which contains login status information.
    //
    // @see RPCManager.loginRequiredMarker
    //
    // @group relogin
    // @visibility external
    //<
    loginStatusCodeMarker: "<SCRIPT>//'\"]]>>isc_",

    //> @classAttr RPCManager.loginRequiredMarker (string : RPCManager.loginRequiredMarker : IRWA)
    // Marker the system will look for in order to detect when login is required.
    // <P>
    // The default loginRequired marker should generally <b>not</b> be customized.  It is
    // designed to be safe to insert into any HTML page or other server response without
    // affecting display or functionality, for example, within an HTML comment.  You should
    // *only* customize the <code>loginRequiredMarker</code> if you have absolutely no ability
    // to change the response that the server will send when login is required.
    // <P>
    // If you do customize the <code>loginRequiredMarker</code>, then the loginRequiredMarker,
    // +link{loginSuccessMarker} and +link{maxLoginAttemptsExceededMarker} should all start with the
    // +link{loginStatusCodeMarker}.  If they do not, there will be a small impact on
    // performance as every response must be separately scanned for each marker, instead of just scanning
    // once for the +link{RPCManager.loginStatusCodeMarker}.
    // <P>
    // In addition, the +link{loginStatusCodeMarker} should ideally contain text that could not possibly
    // validly appear as a data value in a normal response, since if that were possible, end
    // users could enter the loginRequiredMarker as a data value and cause SmartClient to
    // falsely detect session timeout when handling an ordinary data response.  This is why the
    // default marker has characters that make it impossible for it to be validly interpreted
    // as a JavaScript String, XML document or HTML content - there is no way that an end user
    // could enter this as a data value in an application and have it appear verbatim in a
    // server response.
    //
    // @group relogin
    // @visibility external
    //<
    loginRequiredMarker: "<SCRIPT>//'\"]]>>isc_loginRequired",

    // NOTE: the default loginStatusCodeMarker avoids being a valid data value, since:
    // - it can't appear as a JS string or XML attribute (embedded quotes of each type)
    // - it can't appear an a tag value in an XML file (CDATA terminator, followed by additional >)
    // - it can't appear in user-entered HTML content, because the most rudimentary data
    //   cleaners should remove or deactivate <SCRIPT> in user data displayed within HTML

    //> @classAttr RPCManager.loginSuccessMarker (String : RPCManager.loginSuccessMarker : IRWA)
    //
    //  Marker the system will look for in order to detect when login was successfull.
    //
    // @see RPCManager.loginRequiredMarker
    //
    // @group relogin
    // @visibility external
    //<
    loginSuccessMarker: "<SCRIPT>//'\"]]>>isc_loginSuccess",

    //> @classAttr RPCManager.maxLoginAttemptsExceededMarker (String : RPCManager.maxLoginAttemptsExceededMarker : IRWA)
    //
    //  Marker the system will look for in order to detect when the number of maximum logins was exceeded.
    //
    // @see RPCManager.loginRequiredMarker
    //
    // @group relogin
    // @visibility external
    //<
    maxLoginAttemptsExceededMarker: "<SCRIPT>//'\"]]>>isc_maxLoginAttemptsExceeded",

    processLoginStatusText : function (xhr, transactionNum) {
        // don't scan long RPC responses for relogin
        // NOTE: Although the variable is called "xhr", it might actually be a string of HTML,
        // now that we support relogin with the hiddenFrame transport
        var text = xhr.responseText || xhr;
        // In Safari, if you target a file that is empty, the response text is null
        if (text && text.length < this.maxLoginPageLength) {

            // if all the markers start with loginStatusCodeMarker, then only handle markers
            // if loginStatusCodeMarker was found in the text
            if (this.loginSuccessMarker.indexOf(this.loginStatusCodeMarker) != 0
                || this.maxLoginAttemptsExceededMarker.indexOf(this.loginStatusCodeMarker) != 0
                || this.loginRequiredMarker.indexOf(this.loginStatusCodeMarker) != 0) {
            } else {
                var iscIndex = text.indexOf(this.loginStatusCodeMarker);
                if (iscIndex == -1) return false;
            }

            // otherwise scan for each marker individually (slower)
            if (text.indexOf(this.loginRequiredMarker, iscIndex) != -1) {
                this.handleLoginRequired(transactionNum);
                return true;
            } else if (text.indexOf(this.loginSuccessMarker, iscIndex) != -1) {
                this.handleLoginSuccess(transactionNum);
                return true;
            } else if (text.indexOf(this.maxLoginAttemptsExceededMarker, iscIndex) != -1) {
                this.handleMaxLoginAttemptsExceeded(transactionNum);
                return true;
            }
        }
        return false;
    },
    processLoginStatusCode : function (rpcResponse, transactionNum) {
        if (rpcResponse.status == isc.RPCResponse.STATUS_LOGIN_REQUIRED) {
            this.handleLoginRequired(rpcResponse.transactionNum);
            return true;
        } else if (rpcResponse.status == isc.RPCResponse.STATUS_LOGIN_SUCCESS) {
            this.handleLoginSuccess(rpcResponse.transactionNum);
            return true;
        } else if (rpcResponse.status == isc.RPCResponse.STATUS_MAX_LOGIN_ATTEMPTS_EXCEEDED) {
            this.handleMaxLoginAttemptsExceeded(rpcResponse.transactionNum);
            return true;
        }
        return false;
    },
    // extra end-user hook
    responseRequiresLogin : function (xmlHttpRequest, transactionNum) {
        return false;
    },

    // create an RPCResponse, defaulting various fields based on the HTTP transaction it was
    // part of
    createRPCResponse : function (transaction, request, props) {
        var response = isc.addProperties({
            operationId: request.operation.ID,
            // expose the passed clientContext
            clientContext: request.clientContext,
            internalClientContext: request.internalClientContext,
            // backcompat: expose the entirety of the dsRequest as context (flags used to be
            // merged with preserved data)
            context: request,
            transactionNum: transaction.transactionNum,
            httpResponseCode: transaction.httpResponseCode,
            httpResponseText: transaction.httpResponseText,
            xmlHttpRequest: transaction.xmlHttpRequest,
            transport: transaction.transport,
            status: transaction.status,
            clientOnly: request.clientOnly
        }, props);

        if (transaction.transport == "xmlHttpRequest") {
            isc.addProperties(response, {
                httpHeaders: this.getHttpHeaders(transaction.xmlHttpRequest, transaction)
            });
        }

        return response;
    },

    getHttpHeaders : function (xhr, transaction) {


        // A clientOnly request will of course not have an HTTP response of any kind
        if (transaction.allClientOnly) {
            //this.logWarn("Skipping getHttpHeaders for clientOnly request");
            return;
        }

        // Otherwise the xmlHttpRequest should be valid
        if (!xhr) {
            this.logWarn("getHttpHeaders called with a null XmlHttpRequest object");
            return;
        }

        // This check for xhr.getAllResponseHeaders fails with a JS error ("Object does not
        // support method or property") in all versions of Internet Explorer
        if (!isc.Browser.isIE && !xhr.getAllResponseHeaders) {
            // A normal case - probably just indicates that we're in an older browser
            return null;
        }

        var headersString;
        try {
            headersString = xhr.getAllResponseHeaders();
        } catch (e) {
            this.logWarn("Exception thrown by xmlHttpRequest.getAllResponseHeaders(): " + e);
        }

        if (!headersString) {
            this.logWarn("xmlHttpRequest.getAllResponseHeaders() returned null");
            return null;
        }

        var headers = headersString.split('\n');
        var headersObj = {};
        for (var i = 0; i < headers.length; i++) {
            if (headers[i].replace(/^\s+|\s+$/g, '') == "") continue;
            var colonPos = headers[i].indexOf(':');
            if (colonPos == -1) {
                this.logWarn("GetAllResponseHeaders string had malformed entry at line " + 1 +
                             ".  Line reads " + headers[i]);
                continue;
            }
            var key = headers[i].substring(0, colonPos);
            headersObj[key] = headers[i].substring(colonPos+1).replace(/^\s+|\s+$/g, '');
            // Just to be helpful...
            if (headersObj[key] == "true") headersObj[key] = true;
            if (headersObj[key] == "false") headersObj[key] = false;
        }

        // note that 'Set-Cookie' is renamed by the proxy server - copy across if present
        if (headersObj["X-Proxied-Set-Cookie"] != null) {
            headersObj["Set-Cookie"] = headersObj["X-Proxied-Set-Cookie"];
        }
        return headersObj;

    },

    
    _restXMLTransactionStart : "<responses>",
    _restXMLTransactionEnd : "</responses>",
    _restXMLResponseStart : "<response>",
    _restXMLResponseEnd : "</response>",
    _stripXMLTransactionTags : function (restResponse) {
        var startIndex = restResponse.indexOf(this._restXMLTransactionStart);
        if (startIndex != -1) {
            return restResponse.substring(startIndex + this._restXMLTransactionStart.length,
                                        restResponse.lastIndexOf(this._restXMLTransactionEnd));
        }
        return restResponse;
    },
    _getXMLResponses : function (xmlText) {
        xmlText = this._stripXMLTransactionTags(xmlText);
        var responses = [];
        var endIndex = 0;
        if (xmlText) {
            while (true) {
                var startIndex = xmlText.indexOf(this._restXMLResponseStart, endIndex);
                if (startIndex == -1) break;
                endIndex = xmlText.indexOf(this._restXMLResponseEnd, startIndex);
                if (endIndex == -1) break;   // XXX: Should probably log something here...
                responses.add(xmlText.substring(startIndex,
                                                endIndex + this._restXMLResponseEnd.length));
            }
        }
        return responses;
    },
    _stripRestTags : function (restResponse, ds) {
        if (!restResponse || !ds) return restResponse;
        var startIndex = ds.jsonPrefix ? restResponse.indexOf(ds.jsonPrefix) : 0,
            prefixLength = ds.jsonPrefix ? ds.jsonPrefix.length : 0,
            endIndex = ds.jsonSuffix ? restResponse.lastIndexOf(ds.jsonSuffix) : restResponse.length;
        if (startIndex == -1) {
            this.logWarn("DataSource " + ds.ID + ": REST response did not contain the " +
                         "jsonPrefix configured for this DataSource ('" + ds.jsonPrefix +
                         "'). Response evaluation may well fail as a result. Check your " +
                         "server logic and/or DataSource definition.");
            startIndex = 0;
        }
        if (endIndex == -1) {
            this.logWarn("DataSource " + ds.ID + ": REST response did not contain the " +
                         "jsonSuffix configured for this DataSource ('" + ds.jsonSuffix +
                         "'). Response evaluation may well fail as a result. Check your " +
                         "server logic and/or DataSource definition.");
            startIndex = restResponse.length;
        }
        return restResponse.substring(startIndex + prefixLength, endIndex);
    },

    //> @classAttr RPCManager.allowIE9Leak (boolean : true : IRWA)
    // In Internet Explorer 9, when a string of
    // JavaScript is evaluated via the native <code>eval()</code> function, objects created
    // within that evaluation are not released from browser memory until the page is
    // reloaded.
    // <P>
    // SmartClient uses the <code>eval()</code> function to evaluate JSON formatted
    // responses to RPCRequests by default, making long running applications potentially
    // susceptible to memory leaks over time.
    // <P>
    // Note that this does not apply to DataSources which use
    // +link{DataSource.useStrictJSON} formatted responses as the framework avoids
    // calling eval() altogether and makes use of the native browser
    // <code>JSON.parse()</code> method which does not have this issue. By default
    // we also use strict json formatted responses for all
    // +link{DataSource.dataFormat,dataFormat:"iscServer"} dataSources in IE9, so these
    // leaks are mainly a concern only for dataSources with +link{dataSource.dataFormat}
    // set to "json".
    // <P>
    // Setting this property to <code>false</code> enables a workaround suggested on the
    // <a href="http://support.microsoft.com/kb/2572253">Microsoft Knowledge Base</a> to
    // avoid such memory leaks by evaluating script in a hidden iframe and periodically
    // refresh that frame. However developers should be aware of the following
    // limitation with this setting:
    // attempting to access certain object types including
    // <code>Date</code> or <code>function</code> objects generated from such an
    // evaluation can subsequently lead to a JavaScript error with the message
    // <code>"Can't execute code from a freed script"</code>.
    // <P>
    // This workaround therefore may not be suitable for all transactions or dataSources
    // within a given application.
    // <P>
    // This property may also be specified for specific +link{RPCRequest.allowIE9Leak,RPCRequests}.
    // <P>
    // This issue is discussed further in the online
    // <a href="http://forums.smartclient.com/showthread.php?t=8159">SmartClient FAQ</a>.
    //
    // @visibility external
    //<
    allowIE9Leak:true,

    // useJSONParse_IE9
    // This undocumented flag enables a hedge whereby for all iscServer dataSource requests
    // if we're in IE9 we default to using strict JSON (but also have a flag to
    // fallback to eval in case the response doesn't meet strict-JSON formatting restrictions.)
    useJSONParse_IE9:true,

    _performTransactionReply : function (transactionNum) {
        //!OBFUSCATEOK

        // get a pointer to the specified transaction
        var transaction = this.getTransaction(transactionNum);

        // we no longer clear transactions here as they can be suspended during an rpcResponse
        // callback, but we do need to clear the timeout here since the server has definitely
        // replied.
        this.clearTransactionTimeout(transactionNum);

        // The transaction could have timed out, but the server could in theory return just
        // after the timeout.  Since the timeout will have cleared the transaction object,
        // check it here.
        if (!transaction) return;

        //>DEBUG
        if (this.logIsDebugEnabled()) {
            this.logDebug("Result string for transaction "+transactionNum+": " + isc.Log.echoAll(transaction.results));
        }
        //<DEBUG


        var responseIsStructured;
        var allowIE9Leak = false,
            useStrictJSON = false,
            jsonReviverFunction = null;

        if (transaction.operations[0]
                 && transaction.operations[0].allowIE9Leak != null)
        {
             allowIE9Leak = transaction.operations[0].allowIE9Leak;
        } else if (isc.RPCManager.allowIE9Leak) {
            allowIE9Leak = true;
        }
        if (transaction.operations[0] && transaction.operations[0].useStrictJSON != null) {
            useStrictJSON = transaction.operations[0].useStrictJSON;



            jsonReviverFunction = transaction.operations[0].jsonReviver;
        }

        if (isc.Log.logIsInfoEnabled("RpcTabTiming")) {
            transaction.parseOrEvalResponseStart = isc.timeStamp();
        }

        if (transaction.transport == "scriptInclude") {
            // not structured, results are values
        } else if (isc.isAn.Array(transaction.results)) {
            // error - don't do anything here, just drop through to the logic below
            responseIsStructured = true;

        } else if (transaction.allClientOnly) {


            transaction.results = {status:0};
            transaction.receiveTime = isc.timeStamp();
        } else {
            // the results are available as a single string, which we can eval to get JS
            // objects.
            

                // For REST queues, we assume that the settings that apply to the first request
                // apply to all requests in the queue (this assumption is publicly documented)
                var request = transaction.operations[0];
                if (request && request.isRestRequest) {
                    var ds = isc.DataSource.get(request.dataSource);
                    if (ds) {
                        if (request && request.dataFormat == "json" && ds.jsonPrefix == null)
                        {
                            responseIsStructured = true;
                        } else {
                            var restRPCStartIndex = transaction.results ?
                                transaction.results.indexOf(ds.jsonPrefix) : -1;
                            responseIsStructured = restRPCStartIndex != -1;
                        }
                        if (responseIsStructured) {
                            var restResponse = transaction.results;
                            restResponse = this._stripRestTags(restResponse, ds);
                            try {
                                if (useStrictJSON) {
                                    transaction.results =
                                        isc.Class.parseStrictJSON(restResponse, jsonReviverFunction);
                                } else {
                                    // Pass in the 'isJSON' param to evaluate in an IFrame
                                    // in IE9, working around the known IE9 leak on eval.
                                    transaction.results =
                                        isc.eval("(" + restResponse + ")",
                                            (allowIE9Leak ? false: true));
                                }
                            } catch (e) {
                                this.logWarn("Error evaling REST RPC response: " + e
                                             + " response text: " + restResponse);
                            }
                        }
                    }
                }
            

            if (!responseIsStructured) {
                var operation = isc.isAn.Array(transaction.operations) ? transaction.operations[0]
                                                                       : null;
                if (operation && operation.isRestRequest && transaction.operations.length > 1) {
                    // NOTE: We assume that a queue whose first entry is a RestRequest consists
                    // entirely of RestRequests, and that all these RestRequests use the same
                    // dataFormat
                    if (operation.dataFormat == "json") {
                        // This should have already been dealt with by the earlier section
                        // checking for _restRPCStart - we must have a JSON response that was
                        // not wrapped in start/end tags.  This is unexpected, so we'll log it
                        // and bail
                        this.logWarn("Found a REST request that appears to be in JSON format, " +
                            "but the response was not wrapped as configured by the jsonPrefix " +
                            "and jsonSuffix properties - aborting")
                        return;
                    } else {
                        // This is a queued REST request in XML format.  Trim the outer bits off
                        // and then convert the XML string into an array of XML substrings
                        transaction.results = this._getXMLResponses(transaction.results);
                        responseIsStructured = transaction.results != null;
                    }
                }
            }
        }


        if (isc.Log.logIsInfoEnabled("RpcTabTiming")) {
            transaction.parseOrEvalResponseEnd = isc.timeStamp();
        }


        var results = transaction.results;
         if (responseIsStructured && !isc.isAn.Array(results)) results = [results];

        var requests = transaction.operations,
            responses = [];

        // set up a flag noting that we're firing transaction callbacks

        transaction._handlingResponse = true;

        // pair up the requests with their results.
        for (var i = 0, j = 0; i < requests.length; i++) {

            var request = requests[i];

            // Handle Queue (so structured responses)
            // containing both clientOnly requests and real http turnarounds
            // For any clientOnly requests the response is simply "success" and we rely on
            // the callback at the DS level to populate with meaningful results
            var response;
            if (responseIsStructured && request.clientOnly) {
                response = isc.addProperties(this.createRPCResponse(transaction, request), {

                        isStructured: false
                });
            } else {
                response = isc.addProperties(this.createRPCResponse(transaction, request), {

                    isStructured: responseIsStructured,

                    // for scriptInclude, make all values available via callbackArgs - typically
                    // there will be only one - and that's accessible via "data", but all args
                    // are available via this callbackArgs - not currently exposed
                    callbackArgs : transaction.transport == "scriptInclude" ? results : null,

                    // get the results from the server results array;
                    //
                    // if the response is not an rpc, then the same response applies to all
                    // requests.  In general we really expect there to be only one request for non
                    // RPC responses.
                    results: responseIsStructured ? results[j] : results
                });
                if (responseIsStructured && request.isRestRequest) {
                    isc.addProperties(response, {data: results[j]});
                }
                j++;
            }

            // if no status has been set on the response, set to SUCCESS.  This can happen with
            // unstructured responses that simply load a file.
            if (response.status == null) response.status = 0;

            if (response.isStructured) {
                if (response.results.errors) {
                    var errors = response.results.errors;
                    // if the errors array contains only a single map, strip the enclosing array.
                    if (isc.isAn.Array(errors) && errors.length == 1) {
                        errors = errors[0];
                    }
                }


                if (response.results && !request.isRestRequest) {
                    // this makes status, data, startRow, endRow, etc available directly on
                    // "rpcResponse"/"dsResponse", so it's not necessary to use eg
                    // "response.results.data"
                    // XXX: "results" is still a String at this point for XML REST requests,
                    // so we really, really DON'T want to do this for such requests - if we
                    // do, the response will end up with one new attribute per letter of text,
                    // like  0: "<", 1: "r", 2: "e", 3: "s", 4: "p", 5: "o", 6: "n" ... etc ...
                    if (!response.results.response) {
                        isc.addProperties(response, response.results);
                    } else {
                        // For some reason, if the response is reporting a transport error
                        // (a 404, for example), the "results" parameters are wrapped inside
                        // a "response" property, instead of being direct properties of the
                        // results object
                        isc.addProperties(response, response.results.response);
                    }
                }
            }
            responses[i] = response;
            transaction.responses[i] = response;
            transaction.changed();
        }

        var requestNum = 0;
        while (requestNum < requests.length && !transaction.suspended &&
                !transaction.abortCallbacks)
        {
            var request = requests[requestNum],
                response = responses[requestNum];

            if (isc.DataSource) {
                isc.DataSource.recordTimingData(response, "Parse/eval response", "start",
                                                    transaction.parseOrEvalResponseStart);
                isc.DataSource.recordTimingData(response, "Parse/eval response", "end",
                                                    transaction.parseOrEvalResponseEnd);
                isc.DataSource.recordTimingData(response, "performOperationReply", "start");
            }
            this.performOperationReply(request, response);
            if (isc.DataSource) {
                isc.DataSource.recordTimingData(response, "performOperationReply", "end");
            }

            requestNum++;
        }

        // optional transactionComplete API, fired for every transaction
        if (this.transactionComplete != null) {
            this.transactionComplete(transaction);
        }

        if (transaction.showPrompt) this.doClearPrompt(transaction);

        // cleanup the transaction unless it's been suspended
        if (!transaction.suspended && !transaction.abortCallbacks) {
            this.clearTransaction(transactionNum);
        }

        delete transaction.abortCallbacks;
        delete transaction._handlingResponse;

        // if this was an offline transaction, we're in playback mode - playback the next one
        if (transaction.offline) this.playbackNextOfflineTransaction();

        // Log the point at which we hand back to user code
        if (isc.DataSource) {
            var userCallbackInvoked = isc.timeStamp();
            var requestNum = 0;
            while (requestNum < requests.length && !transaction.suspended &&
                    !transaction.abortCallbacks)
            {
                isc.DataSource.recordTimingData(responses[requestNum++], "User callback invoked",
                                                    "start", userCallbackInvoked);
            }

        }

        // fire the callback passed into sendqueue()
        if (transaction._userCallback) {
            var application = request.application ? request.application
                                              : this.getDefaultApplication();
            if (isc.isA.String(application)) application = window[application];

            application.fireCallback(transaction._userCallback, "responses", [transaction.responses]);
        }

        if (isc.DataSource) {
            var userCallbackFinished = isc.timeStamp();
            var requestNum = 0;
            while (requestNum < requests.length && !transaction.suspended &&
                    !transaction.abortCallbacks)
            {
                isc.DataSource.recordTimingData(responses[requestNum], "User callback invoked",
                                                    "end", userCallbackFinished);
                isc.DataSource.recordTimingData(responses[requestNum++], "Client processing",
                                                    "end", userCallbackFinished);
            }

        }
    },

    performOperationReply : function (request, response) {
        var results = response.results,
            operation = request.operation;

        //>DEBUG
        if (this.logIsInfoEnabled()) {
            
                this.logInfo("rpcResponse(unstructured) results -->"
                    + isc.Log.echoAll(results) + "<--");
            
        }
        //<DEBUG

        // support new RPCManager.requestsArePending() API
        this.pendingRpcs--;

        var modifiedResponse = isc.RPC.transformResponse(response, request, request.data);
        response = modifiedResponse || response;

        // support override of login response code handling
        if (this.processLoginStatusCode(response, response.transactionNum)) return;

        

        return this.fireReplyCallbacks(request, response);
    },

    fireReplyCallback : function (callback, request, response, data) {

        var application = request.application ? request.application
                                              : this.getDefaultApplication();
        if (isc.isA.String(application)) application = window[application];
        var callbackResult = application.fireCallback(callback,
                                "rpcResponse,data,rpcRequest", [response, data,request]);
        return callbackResult;
    },

    evalResult : function (request, response, results) {
        var evalVars = request.evalVars;
        this.logDebug("evaling result" + (evalVars ? " with evalVars: " + isc.Log.echo(evalVars) : ""));

        var origAutoDraw = isc.Canvas.getInstanceProperty("autoDraw");
        if (request.suppressAutoDraw) isc.Canvas.setInstanceProperty("autoDraw", false);
        

        // results at this point is the string to eval
        // eval barfs on "{ ...  }", thinking it's a closure - work around this so developers
        // can return simple JSON format objects
        // Note: FF1.0.7 bug: curly must be escaped in the following regex.  Not an issue in IE
        // or FF1.5
        if (results.match(/^\s*\{/)) {

            results = "var evalText=" + results + ";evalText;"
        }


        var evalResult = isc.Class.evalWithVars(results, evalVars);
        if (request.suppressAutoDraw) isc.Canvas.setInstanceProperty("autoDraw", origAutoDraw);
        return evalResult;
    },

    // called by performOperationReply
    fireReplyCallbacks : function (request, response) {
        var operation = request.operation,
            results = response.results,
            // ignore evalResult for scriptInclude - not relevant
            evalResult = request.evalResult && request.transport != "scriptInclude" ?
                this.evalResult(request, response, results) : null
        ;

        // for RPC rsponses, 'data' is the data value in the response object literal.  For
        // evalResult, it's the result of the eval (see above).  For serverOutputAsString, it's
        // the server string
        var data;
        
            data = (request.evalResult ? evalResult : results);

        // always have 'data' be available on the response object as well
        response.data = data;

        var transaction = this.getTransaction(response.transactionNum);

        // fire all callbacks set on the request
        // call callbacks:
        // - request.callback is passed to RPCManager.send/sendRequest
        // - context.afterFlowCallback is passed to actionMethods such as ListGrid.fetchData(),
        //   generally by application code that needs to do something after a canonical flow
        //   completes
        // Really, "flows" implemented on top of the RPC mechanism should provide their own,
        // chained callback to any higher layer.  Instead, for the moment, the RPCManager fires
        // exactly two level of callbacks in series.
        var callback = request.callback;
        if (callback != null) {

            // use fireCallback() to fire the callback
            this.fireReplyCallback(callback, request, response, data);
        }
    },

    // Error handling
    // --------------------------------------------------------------------------------------------

    //> @classMethod RPCManager.handleError()
    // <code>handleError</code> will be called if +link{rpcResponse.status} is negative and
    // +link{rpcRequest.willHandleError} was not set.
    // <p>
    // This method is called for both +link{DSResponse}s and +link{RPCResponse}s that have a
    // non-success status.  You can check whether the response is a DSResponse by checking
    // <code>response.isDSResponse</code>.
    // <p>
    // By default <code>handleError()</code> always logs a warning.  In addition, if
    // +link{rpcResponse.data, response.data} was set to a String, a warning dialog will be
    // shown to the user with response.data as the message, which allows the server to send
    // user error messages back without writing custom client-side error handling.
    // <p>
    // To do custom error handling that is specific to a particular component or type of
    // request, set +link{rpcRequest.willHandleError} and deal with errors in the
    // rpcRequest.callback.  To change the default system-wide error handling, override this
    // method.  Note that since <code>handleError()</code> is a class method, to override it
    // you will call +link{Class.addClassProperties(),addClassProperties()} rather than
    // addProperties(), like so:
    // <pre>
    //     isc.RPCManager.addClassProperties({
    //         handleError : function (response, request) { .. custom handling .. }
    //     })
    // </pre>
    // <p>
    // If you're using the xmlHttpRequest +link{rpcRequest.transport}, you can access the
    // +externalLink{http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html,HTTP status code}
    // of the response (eg 404 Not Found or 500 Server Error) as +link{rpcResponse.httpResponseCode}.
    // <P>
    // For very advanced usage, the response.xmlHttpRequest contains the native XMLHttpRequest
    // object used to make the request.  Accessing this object is subject to possible
    // cross-platform bugs and inconsistencies, and Isomorphic recommends that you wrap any
    // access to the XMLHttpRequest object in a try/catch block because some browsers may throw
    // exceptions when certain attributes of this object are accessed.  For example, if you try
    // to access XMLHttpRequest.status (for the HTTP status code) when the network cable
    // is unpluged in Windows, you'll get an Exception in Firefox.
    //
    // @param response (Response) the RPCResponse or DSResponse object returned from the server
    // @param request (Request) the RPCRequest or DSRequest that was sent to the server
    //
    // @see DataSource.handleError()
    // @group errorHandling
    // @group operations
    // @visibility external
    //<

    //> @method Callbacks.HandleErrorCallback
    // Called when a session timeout is encountered while trying to do a background RPC.
    //
    // @param response (DSResponse) response the response
    // @param request (DSRequest) request the request
    //
    // @visibility sgwt
    //<
    _handleError : function (response, request) {
        if (response.ignoreError) return;

        // if this is a dsRequest, check for a handleError() method on the DataSource and call
        // that if it exists
        if (request.dataSource) {
            var ds = isc.DataSource.get(request.dataSource);
            if (ds && ds.handleError) {
                // call the handler - if it returns false, suppress call to handleError
                var val = ds.handleError(response, request);
                if (val == false) return;
            }
        }
        return this.handleError(response, request);
    },
    handleError : function (response, request) {
        var context = (response.context ? response.context : {}),
            message;
        if (isc.isA.String(response.data)) {
            // this lets the developer show an arbitrary error message generated by the server,
            // for things like authorization code that a developer doesn't want to build
            // client-side
            message = response.data;
            if (isc.isA.String(message)) {
                var messageToShow = message;
                if (messageToShow.length > this.maxErrorMessageLength) {
                    var delta = messageToShow.length - this.maxErrorMessageLength;
                    messageToShow = messageToShow.substring(0, this.maxErrorMessageLength)
                                    + "<br><br>...("+delta+" bytes truncated - set"
                                    + " isc.RPCManager.maxErrorMessageLength > "
                                    + this.maxErrorMessageLength
                                    + " to see more or check the Developer Console for full error)...";
                }
                this.reportError(messageToShow);
            }
        } else {
            // find the name of the constant for the reported error num, if there is one
            var codeName = isc.getKeyForValue(response.status, isc.RPCResponse.errorCodes);
            if (isc.isA.String(codeName)) {
                if (codeName.startsWith("STATUS_")) codeName = codeName.substring(7);
            } else {
                codeName = (response.status != null ? "error code: " + response.status
                                                    : "unknown error code");
            }
            var opName = response.operationId || response.operationType,
                extraText = "";

            if (codeName == "MAX_FILE_SIZE_EXCEEDED") {
                message = isc.DataSource.maxFileSizeExceededMessage.evalDynamicString(this, {
                        maxFileSize: response.maxFileSize,
                        uploadedFileName: response.uploadedFileName,
                        uploadedFileSize: response.uploadedFileSize
                    });
                extraText = "\nSet rpcRequest.willHandleError:true on your request to handle this " +
                    "error yourself, or add a custom handleError to RPCManager to change " +
                    "system-wide default error reporting";
            } else if (codeName == "VALIDATION_ERROR") {
                message = "Server returned validation errors: " + isc.echoFull(response.errors);
                extraText = "\nSet rpcRequest.willHandleError:true on your request to handle this " +
                    "error yourself, or add a custom handleError to RPCManager to change " +
                    "system-wide default error reporting";
            } else {
                message = "Server returned " + codeName + " with no error message" +
                        (opName ? " performing operation '" + opName + "'." : ".");
            }

            this.reportError(message);

        }
        // log regardless
        this.logWarn(message + extraText + " - response: " + this.echo(response));

        // return false meaning don't proceed
        return false;
    },

    //> @classMethod RPCManager.handleTransportError()
    // Method to handle server error responses to submitted transactions.
    // When the server responds to a submitted transaction with an HTTP error code this method will
    // be called before any individual response callbacks are fired, regardless of whether
    // +link{RPCRequest.willHandleError} was specified on the submitted request[s].
    // <P>
    // This provides the developer with an opportunity to handle a server error by (for example)
    // suspending and resubmitting the transaction before any other handling occurs.
    // <P>
    // The default implementation takes no action - by default transport errors
    // are handled via +link{RPCManager.handleError()}, or by the standard request callback
    // methods, depending on request.willHandleError.
    // To perform custom handing for transport errors this classMethod may be overridden as
    // follows
    // <pre>
    //     isc.RPCManager.addClassProperties({
    //         handleTransportError : function (transactionNum, status, httpResponseCode, httpResponseText)
    //         {
    //                .. custom handling ..
    //         }
    //     })
    // </pre>
    // <P>
    // Note: This method only applies to operations submitted via
    // +link{RPCTransport,XMLHttpRequest} - it is not possible to provide similar error handling
    // for other transports.
    //
    // @param transactionNum (int) The submitted client-server transaction number
    // @param status (integer) The RPCResponse status code
    // @param httpResponseCode (integer) The HTTP Response code reported by the server
    // @param httpResponseText (text) The raw HTTP Response text
    //
    // @group errorHandling
    // @visibility external
    //<


    //> @method Callbacks.HandleTransportErrorCallback
    // RPCManager transport error callback.
    //
    // @param transactionNum (int) The submitted client-server transaction number
    // @param status (int) The RPCResponse status code
    // @param httpResponseCode (int) The HTTP Response code reported by the server
    // @param httpResponseText (text) The raw HTTP Response text
    //
    // @visibility sgwt
    //<

    handleTransportError : function (transactionNum, status, httpResponseCode, httpResponseText) {
    },


//> @groupDef relogin
//
// When a user's session has expired and the user tries to navigate to a protected resource,
// typical authentication systems will redirect the user to a login page.  With Ajax systems
// such as SmartClient, this attempted redirect may happen in response to background data
// operations such as a form trying to save.  In this case, the form perceives the login page
// as a malformed response and displays a warning, and the login page is never displayed to the
// user.
// <P>
// The ideal handling of this scenario is that the form's attempt to save is "suspended" while the
// user re-authenticates, then is completed normally.  SmartClient makes it easy to
// implement this ideal handling <i>without</i> having to implement session timeout handling in
// every codepath that contacts the server, by providing central notification of session timeout,
// and the ability to re-send a transaction that encountered session timeout.
// <P>
// <h3>Detecting session timeout</h3>
// <P>
// To enable SmartClient to detect that session timeout has occurred, a special marker needs to
// be added to the HTTP response that is sent when a user's session has timed out.  This is
// called the <code>loginRequiredMarker</code>.
// <P>
// If your authentication system will redirect to a login page when a user's session is timed
// out, it's sufficient to simply embed the <code>loginRequiredMarker</code> in the login page.  The
// <code>loginRequiredMarker</code> is valid HTML and will have no effect on the behavior or
// appearance of the page.  The <code>loginRequiredMarker</code> is found in
// <smartclient>smartclientSDK/isomorphic/login/loginRequiredMarker.html</smartclient>
// <smartgwt>docs/loginRequiredMarker.html</smartgwt>
// in your SDK.  Simply copy the contents of this file verbatim into your login page anywhere
// inside the &lt;body&gt; tag; it does not need to be customized in any way for your application.
// <P>
// If it's a problem to modify the login page (even with a marker that has no effect on
// appearance or behavior), see if you can configure your authentication system to return a
// special response specifically for background requests for data.  By default, when using the
// SmartClient Server Framework, all such requests go to the +link{RPCManager.actionURL} and
// include an HTTP query parameter "isc_rpc=1"; various authentication systems can be
// configured to detect these requests and handle them separately.  One approach is to simply
// copy loginRequiredMarker.html into your application in an area not protected by
// authentication and redirect to it when a background data request with an expired session is
// detected.
// <P>
// <h3>Handling session timeout</h3>
// <P>
// When SmartClient detects the <code>loginRequiredMarker</code>, the transaction that
// encountered session timeout is put on hold, and
// <smartclient>+link{RPCManager.loginRequired()} is called.</smartclient>
// <smartgwt>the RPCManager LoginRequired event is raised.</smartgwt>
// At this point you have a few options:
// <ol>
//
// <li> Leave the SmartClient application and take the user to the login page, by simply doing a
// <code>window.location.replace(<i>myLoginURL</i>)</code>, the simplest but least user friendly
// option.
//
// <li> Open a new browser window that goes to your plain HTML login form (or offer a link that
// opens such a browser window), using a modal dialog in the application page that prompts the
// user to login before continuing, then re-send the intercepted transaction
// (+link{RPCManager.resendTransaction()} when the user indicates he has logged in.
// This is simple, does not drop context, but is not seamless.
//
// <li> Use a SmartClient interface, typically a DynamicForm in a Window, to collect credentials,
// perform login as a background RPC, and on success re-send the intercepted transaction
// (+link{RPCManager.resendTransaction()}.  <smartclient>A complete example of this,
// which assumes an authentication system that can take credentials as HTTP POST params, is
// included in the SDK as isomorphic/login/reloginFlow.js.</smartclient>
//
// </ol>
// <B>Authentication via background RPC form POST</B>
// <smartclient>
// <P>
// The approach shown in reloginFlow.js posts the credentials gathered from the user to
// +link{RPCManager.credentialsURL}.  To make this work with an authentication system that can
// accept credentials via HTTP POST:
// <ol>
// <li> set the RPCManager.credentialsURL to the URL where credentials should be POST'd
// <li> include reloginFlow.js in your page, modified, if necessary, so that the names of the
// USERNAME and PASSWORD params match what your authentication system uses
// <li> configure your authentication system to send back the loginSuccessMarker as part of a
// successful login response, and the loginRequiredMarker as part of a failed login response
// </ol>
// If your authentication system can accept POST'd credentials at any URL it protects, the last
// step may be as simple as configuring the loginSuccessMarker file itself as a protected
// resource (<code>isomorphic/login/loginSuccessMarker.html</code>).
// </smartclient>
// <smartgwt>

// <P>
// To relogin against any system that can accept credentials as an HTTP POST:
// <ol>
// <li> when the LoginRequired event is raised, show a login form in a modal dialog.  The
//      +link{isc.showLoginDialog,LoginWindow} component is a simple version of this, or you can create your own
// <li> when the user enters credentials, POST them using code like the following:
// <pre>
//    RPCRequest request = new RPCRequest();
//    request.setContainsCredentials(true);
//    request.setActionURL(credentialsURL);
//    request.setUseSimpleHttp(true);
//    request.setShowPrompt(false);
//    Map<String,String> params = new HashMap<String,String>();
//    // adjust parameter names to match your authentication system
//    params.put("j_username",<i>username</i>);
//    params.put("j_password",<i>password</i>);
//    request.setParams(params);
//    RPCManager.sendRequest(request,new RPCCallback(){
//        public void execute(RPCResponse response, Object rawData, RPCRequest request) {
//            if (response.getStatus() == RPCResponse.STATUS_SUCCESS) {
//                // get rid of login window
//                RPCManager.resendTransaction();
//            } else if (response.getStatus() == RPCResponse.STATUS_LOGIN_INCORRECT) {
//                // show an error in the login window
//            }
//        }
//    });
// </pre>
// <li> configure your authentication system to send back the loginSuccessMarker as part of a
// successful login response, and the loginRequiredMarker as part of a failed login response
// </ol>
// If your authentication system can accept POST'd credentials at any URL it protects, the last
// step may be as simple as configuring the <code>loginSuccessMarker</code> file itself as a
// protected resource.  The <code>loginSuccessMarker</code> is found in
// <code>docs/loginSuccessMarker.html</code> in your SDK.
// </smartgwt>
// <P>
// <B>Authentication via background SmartClient server RPC/DMI</B>
// <P>
// If you are using the SmartClient Java server and your authentication system allows you to mark
// a user as authenticated from Java, you can perform a normal RPC or DMI with the credentials
// gathered from the user and send back success or failure indications as normal RPC or DMI
// responses.  This can be useful if, in addition to logging in, you want to send back additional
// data.
// <P>
// <B>Advanced: concurrency</B>
// <P>
// If, after loginRequired() has fired and before the user has re-authenticated, you send
// additional RPCs to protected URLs, you will get additional loginRequired() notifications.  This
// may happen to applications that poll for data or periodically save without user action.  You
// may wish to avoid this by setting an application-specific flag to avoid firing requests during
// the relogin process.  However, you must ultimately either
// +link{RPCManager.resendTransaction(),resend} or +link{RPCManager.clearTransaction(),discard}
// every transaction for which loginRequired() fires, or you will have a memory leak due to
// suspended transactions.
// <P>
// Note also that there is no requirement that the relogin process blocks user interaction.
// Applications that access multiple services may choose to simply show an unobtrusive error
// indication such that the user can log back in at his leisure, or even log the user back in
// automatically.
//
// @title Relogin
// @visibility external
// @treeLocation Client Reference/RPC
//<



    // Relogin design:
    //  - NOTE: relogin is unlike other error codes that might be returned in an RPCResponse
    //    because the ideal handling is to process relogin and then proceed with the original
    //    request, transparently to the requesting code.  Since the requesting code ultimately
    //    receives a normal response, representing a login failure as an RPCResponse would mean
    //    two responses to the same request
    //  - two types of interceptor:
    //    - 1. server code is capable of adding a canned snippet to indicate login required
    //      - this is how the SmartClient server authenticator works
    //      - this covers SiteMinder-style interceptors that send back HTML pages
    //    - 2. non-ISC backends that send WSDL, XML or JSON responses with error codes, which
    //        cannot be modified to return special snippets, hence must be interpreted by
    //        custom logic in the client-side DataSource layer
    //    - NOTE: the above only covers detection scenarios - the other half of the problem is
    //      how to pull off reauthentication in the background with various types of
    //      authentication systems
    //
    // - SiteMinder-style intercept relogin flow: authentication system will send back an HTML
    //   page rather than a clean RPC response
    //   - a snippet added to the login page calls rpc.loginRequired
    //     - in xmlHttp this is detected by scanning the response.  In hiddenFrames comm the
    //       code executes automatically and calls RPC layer
    //   - relogin flows:
    //     - case 1: login is possible via get/post of params or other background RPC
    //     - case 2: login is only possible via using the login form sent back by the
    //       interceptor, because eg it writes out a form with some state in it
    //   - for case 1, you could:
    //     1. use an ISC-based relogin form to gather credentials from the user
    //     2. relogin via RPC, either:
    //        a. as a normal RPC with the ISC server
    //        b. for non-ISC server: embed standardized response markers that ISC recognizes as
    //           success vs failure
    //          - NOTE: someone could roll their own success/failure detection here, but it's
    //            complicated to do so if you need hiddenFrames comm backCompat, and adds
    //            complexity to the explanation
    //     3. call resubmitTransaction() once relogin has been achieved
    //     .. or your could take the approach for case 2, which might be simpler
    //   - for case 2
    //     1. use either an ISC Window or a separate browser window to show the actual HTML
    //        login form sent by the interceptor.  This window (either kind) should be directed
    //        to isomorphic/login/loginSuccessMarker.html and this resource should be
    //        protected by the interceptor such that, after successful login,
    //        loginSuccessMarker.html is loaded
    //        - reloginSuccess.html has a snippet of code that calls rpc.loginSuccess, the
    //          default implementation being to resubmit transactions that were intercepted
    //
    // - DataSource error code-based relogin
    //   - need for relogin is indicated by some error code that can only be detected in the
    //     DataSource layer
    //   - NOTE: in eg DataSource.transformResponse() the appropriate spot to interpret error
    //     codes, we are still in a position to transparently resubmit an intercepted request:
    //     - the custom DS layer is responsible for firing the DBComponent-level callback, so
    //       can leave the DBComponent in limbo during relogin interaction
    //     - the RPC layer will not remove the prompt until the DS layer is done processing
    //   - call suspendTransaction(dsResponse.transactionNum)
    //   - show UI and accomplish relogin as for SiteMinder case
    //
    //  Note: while we are handling relogin, eg, waiting for the user to type in credentials,
    //  further requests to the same protected resource will trigger repeated loginRequired()
    //  notifications.  This situation is fairly rare but could arise with eg, polling.
    //  We shouldn't try to handle this by blocking all RPCs because an application may consume
    //  multiple services and requiring login may not be an application-halting event (eg,
    //  multi-service IM client).  So we expect that in this case, the application developer
    //  either sets a flag to avoid doing more RPCs during relogin or handles multiple
    //  loginRequired() notifications.
    //  Note that, for the case of polling in particular, it is basically required that either
    //  polling stops or polling requests are cleared immediately from loginRequired(), as
    //  otherwise if the application hits session timeout with the user not around, multiple
    //  hours worth of polling requests might queue up before the user comes back.
    handleLoginRequired : function (transactionNum) {
        // in the default flow at the bottom of this method, we open a new browser window to
        // the login page - that'll call handleLoginRequired(), so ignore that call so we don't
        // loop
        if (this._iscReloginWindow && this._iscReloginWindow == transactionNum) return;

        var transaction = this.getTransaction(transactionNum);

        // bogus call - default login.html with a new window - e.g. password-protected
        // DevConsole
        if (transaction == null) return;



        transactionNum = transaction.transactionNum;

        // clear the timeout on the transaction to let the user deal with reauth without getting
        // a timeout - this is universally useful, so we do it by default.  If the user wants
        // to cancel the transaction due to the auth delay, he's still free do so in a custom
        // rpc.loginRequired() implementation
        this.clearTransactionTimeout(transaction);

        var rpcRequest = transaction.operations[0],
            rpcResponse = this.createRPCResponse(transaction, rpcRequest);

        this.logInfo("loginRequired for transaction: " + transactionNum +
                     (transaction.containsCredentials ?
                      ", transaction containsCredentials" : ""));

        if (transaction.containsCredentials) {
            // the user used an RPC to attempt a login, but auth failed for some reason - don't
            // if a callback was set on the request, then the user is expected to be able to
            // handle the AUTH_REQUIRED state - otherwise we'll just call loginRequired()
            // again.
            if (rpcRequest.callback) {
                rpcResponse.status = isc.RPCResponse.STATUS_LOGIN_INCORRECT;
                this.fireReplyCallbacks(rpcRequest, rpcResponse);

                this.clearTransaction(transaction);
                return;
            }
            this.clearTransaction(transaction);
        }

        this.suspendTransaction(transaction);

        // A non-auth RPC was trapped by auth required.
        if (this.loginRequired) {
            // user has specified a custom loginRequired function , call through
            rpcResponse.status = isc.RPCResponse.STATUS_LOGIN_REQUIRED;
            this.loginRequired(transactionNum, rpcRequest, rpcResponse);
            return;
        }

        // default implementation - just open a new window to the login page.  This is
        // guaranteed to work, but isn't the best solution since the user can just close the
        // login window and then be forced to reload the app

        // don't allow page to be cached
        var URL = this.addParamsToURL(this.credentialsURL, {ts:new Date().getTime()});
        this._iscReloginWindow = window.open(URL, this.loginWindowSettings);
    },

//> @classMethod RPCManager.loginRequired()
// Called when a session timeout is encountered while trying to do a background RPC.  See
// +link{group:relogin,Relogin}.
// <P>
// The transaction with the passed <code>transactionId</code> is suspended, and should either
// be +link{RPCManager.clearTransaction,cleared} or +link{RPCManager.resendTransaction,resent}
// after the user has been re-authenticated.
// <P>
// The <code>rpcRequest</code> parameter can be used to determine whether the suspended
// transaction can simply be dropped (eg, it's periodic polling request).
// <P>
// The <code>rpcResponse</code> parameter has rpcResponse.data set to the raw text of the
// response that triggered <code>loginRequired()</code>.  Some very advanced relogin strategies
// may need to inspect the raw response to get information needed for re-authentication.
//
// @param transactionNum (int) id of the transaction
// @param rpcRequest (RPCRequest) first RPCRequest of the transaction
// @param rpcResponse (RPCResponse) RPCResponse containing the session timeout response that
//                                  caused loginRequired() to be invoked
// @group relogin
// @visibility external
//<

//> @method Callbacks.LoginRequiredCallback
// Called when a session timeout is encountered while trying to do a background RPC.
//
// @param transactionNum (int) id of the transaction
// @param rpcRequest (RPCRequest) first RPCRequest of the transaction
// @param rpcResponse (RPCResponse) RPCResponse containing the session timeout response that
//                                  caused loginRequired() to be invoked
//
// @visibility sgwt
//<

    // called by AuthenticationFilter when relogin succeeds (by way of normal
    // performOperationReply() and transaction.containsCredentials detection and by canned "dumb http
    // server" code snippet in reloginSuccess.html
    handleLoginSuccess : function (transactionNum) {
        var transaction = this.getTransaction(transactionNum);
        // transaction may be null if the user popped a visible iframe that targets
        // loginSuccess and is using that as the UI for the user instead of e.g. a SmartClient
        // form that makes login RPCs
        if (transaction && transaction.containsCredentials) {
            this.clearTransactionTimeout(transaction);
            var rpcRequest = transaction.operations[0];
            if (rpcRequest.callback) {
                var rpcResponse = this.createRPCResponse(transaction, rpcRequest, {
                    status: isc.RPCResponse.STATUS_SUCCESS
                });
                this.fireReplyCallbacks(rpcRequest, rpcResponse);
                this.clearTransaction(transaction);
                return;
            }
            this.clearTransaction(transaction);
        }

        if (this._iscReloginWindow) this._iscReloginWindow.close();
        if (this.loginSuccess && this.loginSuccess() === false) return;

        // default handling
        this.resendTransaction();

    },

    // called by AuthenticationFilter or by canned "dumb http server" code snippet in
    // maxLoginAttemptsExceeded.html when too many login attempts have been made
    handleMaxLoginAttemptsExceeded : function (transactionNum) {
        var transaction = this.getTransaction(transactionNum);
        // transaction may be null if the user popped a visible iframe that targets
        // loginSuccess and is using that as the UI for the user instead of e.g. a SmartClient
        // form that makes login RPCs
        if (transaction && transaction.containsCredentials) {
            this.clearTransactionTimeout(transaction);
            var rpcRequest = transaction.operations[0];
            if (rpcRequest.callback) {
                var rpcResponse = this.createRPCResponse(transaction, rpcRequest, {
                    status: isc.RPCResponse.STATUS_MAX_LOGIN_ATTEMPTS_EXCEEDED
                });
                this.fireReplyCallbacks(rpcRequest, rpcResponse);
                this.clearTransaction(transaction);
                return;
            }
            this.clearTransaction(transaction);
        }

        // default handling
        if (this._iscReloginWindow) this._iscReloginWindow.close();

        if (this.maxLoginAttemptsExceeded) this.maxLoginAttemptsExceeded();
        else {
            var message = "Max login attempts exceeded.";
            if (isc.warn) isc.warn(message);
            else alert(message);
        }
    },

    // HTML needed to continue event processing when child window is opened
    _$fireOpenerTimeoutsHTML: "<HTML><SCRIPT>" + "setInterval(function () {" +
        "window.opener.isc.Timer.firePendingTimeouts();},1);</SCRIPT></HTML>",

    //> @classMethod RPCManager.exportContent()
    // Converts +link{canvas.getPrintHTML,printable HTML} generated from live UI components
    // into a .pdf and downloads it ("Save As.." dialog).
    // <P>
    // For +link{DrawPane} and subclasses (e.g. +link{FacetChart}) to export properly, the canvas
    // parameter must be the widget itself, not the HTML obtained with +link{canvas.getPrintHTML(),
    // getPrintHTML()} unless the +link{PrintProperties} passed to getPrintHTML() had
    // +link{PrintProperties.printForExport,printForExport}:true.
    // <P>
    // You can use a custom skin when exporting your HTML content. To use a custom skin,
    // add a line to +link{group:server_properties,server.properties}:
    // <pre>
    //   skin.{skinName}.location: custom/skin
    // </pre>
    // Where {skinName} is the name of your custom skin, and the value is the path to your
    // skin resources from the application webroot.
    // <P>
    // Requires the SmartClient server framework, but does not require use of server-based
    // databinding - no .ds.xml files need to exist.
    // <P>
    // See server-side docs for com.isomorphic.contentexport.PdfExport for more details on
    // server-side processing and code samples for redirecting PDF output to a file or
    // in-memory buffer, as well as instructions for adding additional stylesheets.
    //
    // @param canvas (Canvas | Array[] of Canvas | HTMLString) Canvas or canvas list that has exportable widgets,
    //                    or HTML fragment derived from +link{canvas.getPrintHTML(),getPrintHTML()}
    // @param [requestProperties] (DSRequest properties) Request properties for the export to pdf object
    // @example pdfExportCharts
    // @visibility external
    //<

    exportContent : function (canvas, requestProperties) {
        if (requestProperties == null) requestProperties = {};

        var defaultSkinName = isc.Page.getSkinDir(),
            skinName = defaultSkinName.trim("/").split("/").last();

        // define the settings to pass to getPdfObject
        var settings = {
            pdfName: requestProperties.exportFilename ||
                     requestProperties.pdfName || "export",
            skinName: requestProperties.skinName || skinName,
            defaultSkinName: defaultSkinName
        };

        // define RPCRequest properties
        var serverProps = isc.addProperties({
            showPrompt: false,
            transport: "hiddenFrame",
            exportResults: true,
            downloadResult: true,
            downloadToNewWindow: false,
            download_filename: null
        }, requestProperties);

        // remove settings-specific properties
        delete serverProps.pdfName;
        delete serverProps.skinName;


        if (isc.Browser.isMobileSafari && serverProps.downloadToNewWindow == true) {
            var windowName = serverProps.newDownloadWindow =
                serverProps.download_filename || "request_" + isc.timeStamp();
            window.open(null, windowName).document.write(this._$fireOpenerTimeoutsHTML);
        }

        // generate the print HTML and request PDF from server
        var callback = function (html) {
            isc.DMI.callBuiltin({
                methodName: "getPdfObject",
                arguments: [ html, settings ],
                requestParams: serverProps
            });
        };
        var HTML = isc.Canvas.getPrintHTML(canvas, {
            printForExport: true
        }, callback);


        //if (HTML != null) {
        //    callback(HTML);
        //}
    },

    //> @classMethod RPCManager.exportImage()
    // Converts an +link{drawPane.getSvgString,SVG string} to one of several possible image formats,
    // and can either initiate a download or return the base64-encoded image data.
    // <p>
    // Control the image format via +link{dsRequest.exportImageFormat}.
    // <p>
    // Default is to download the image (triggering the browser's save dialog).
    // +link{dsRequest.exportFilename} can be used to control the default filename provided in the
    // save dialog.
    // <p>
    // To instead return the data as a normal DSResponse, set +link{dsRequest.exportDisplay} to
    // "return".  In this case the data is always base64 encoded.
    // <p>
    // Requires the SmartClient server framework, with the same set of
    // +link{group:javaModuleDependencies,required .jars} as are required for PDF export of charts in
    // legacy IE.
    // <p>
    // See also +link{drawPane.getSvgString()} and +link{drawPane.getDataURL()}.
    //
    // @param svgString (String) XML string containing SVG data
    // @param [requestProperties] (DSRequest Properties) request properties controlling options for export
    // @param [callback] (ExportImageCallback) optional callback when using
    //                                         <code>exportDisplay</code>:"return".  <b>Does not
    //                                         fire</b> for other <code>exportDisplay</code> modes
    // @example chartImageExport
    // @visibility external
    //<
    _$return: "return",
    exportImage : function (svgString, requestProperties, callback) {
        var props = requestProperties || {},
            returnImageData = (props.exportDisplay != this._$return);
        var serverProps = {
            downloadResult: returnImageData
        };
        isc.DMI.callBuiltin({
            methodName: "exportImage",
            arguments: [ svgString, requestProperties ],
            requestParams: serverProps,
            callback : function _handleExportImageReply(rpcResponse, data, rpcRequest) {
                if (callback) {
                    isc.Class.fireCallback(callback, "imageData", [data.base64]);
                }
            }
        });
    },

    //> @method Callbacks.ExportImageCallback
    // Callback for +link{RPCManager.exportImage}.
    // @param imageData (String) image data from the server, in base64 format
    // @visibility external
    //<

    // API to report errors for RPC requests/responses
    reportError : function (errorMessage) {
        isc.warn(errorMessage.asHTML());
    },
    // transformResponse API, similar to the one for DataSource
    transformResponse : function (rpcResponse, rpcRequest, data) {
        return rpcResponse;
    },
    // transformRequest API, similar to the one for DataSource
    transformRequest : function (rpcRequest) {
        return rpcRequest.data;
    },

    //> @classMethod RPCManager.loadScreen()
    // Loads a screen saved in +link{group:componentXML,Component XML} format, using the
    // +link{group:servletDetails,ScreenLoaderServlet}.
    // <P>
    // The ScreenLoaderServlet will look for a file named <i>screenName</i>.ui.xml in
    // the directory given by the "project.ui" setting, which defaults
    // <i>webroot</i>/shared/ui and can be configured in
    // +link{group:server_properties,server.properties}.
    // <p>
    // The <code>screen</code> provided by the callback will be the outermost component if your
    // loaded screen consists of a hierarchy of widgets all contained under one parent (which
    // is true of any screens created in Visual Builder).
    // <p>
    // If you have multiple widget hierarchies in your screen, the <code>screen</code> returned
    // will be the last top-level component created.
    // <P>
    // By default, components in the loaded screens that have +link{Canvas.ID,global IDs} will not
    // actually be allowed to take those global IDs - instead, only widgets that have one of the
    // global IDs passed as the <code>globals</code> parameter will actually receive their global
    // IDs.  To override this behavior, pass the special value +link{RPCManager.ALL_GLOBALS}
    // for the <code>globals</code> parameter.
    // <p>
    // When globals are being suppressed, the <code>screen</code> available in the callback
    // will provide access to widgets that did not receive their global IDs via
    // +link{canvas.getByLocalId()}, and the <code>suppressedGlobals</code> available in the
    // callback will be a mapping from suppressed global ID to the widget or other component
    // that would have used that global ID if globals were not suppressed.  In addition, any
    // other <code>Canvas</code> loaded with the screen also provides access to any suppressed
    // globals from the screen via <code>getByLocalId()</code>.
    // <p>
    // To load multiple screens at once, use +link{cacheScreens()} and +link{createScreen()}
    // instead.
    // <P>
    // Components in the screen will default to having +link{canvas.autoDraw} set to false.
    // This may be overridden by setting the +link{RPCRequest.suppressAutoDraw} attribute
    // explicitly to <code>false</code> on the request properties object.
    // <P>
    // You can optionally provide a locale name to use when resolving any i18n tags in the
    // screen's component XML.  If you do not supply this, the locale will be derived from
    // the servlet API, and so will generally be a locale appropriate to the client's operating
    // system settings.  Only provide a locale manually if you have a special requirement that
    // requires the user's operating system locale to be overridden in your application.  If
    // you provide a locale name, it should be of the form "xx" or "xx_YY", where "xx" is a
    // valid language code and "YY" is a valid country code.  For example, "fr" or "en_GB".
    // <P>
    // This API assumes the ScreenLoaderServlet is installed at the default location - to use a
    // different location, use the <code>requestProperties</code> parameter to specify a different
    // URL via +link{rpcRequest.actionURL}.  The <code>requestProperties</code> parameter can also
    // be used to pass additional params to a custom ScreenLoaderServlet - see the "Dynamic
    // Component XML" section of the +link{group:componentXML,Component XML overview}.
    //
    // @param screenName (String) name of the screen to load
    // @param callback (LoadScreenCallback) callback for notification of screen being loaded
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @param [locale] (String) The name of a locale to use for resolving i18n tags in the
    //        component XML of the screen
    // @param [requestProperties] (RPCRequest Properties) optional properties for the request
    //
    // @visibility external
    //<
    loadScreen : function (screenName, callback, globals, locale, requestProperties) {
        if (!screenName) {
            this.logWarn("No screen names passed in.");
            this.fireCallback(callback, "data", [null]);
            return;
        }
        // Note: this logic will handle multiple screens, but this is intentionally not
        // documented and not supported.  The right way to load multiple screens is to use
        // RPCManager.cacheScreens().
        if (!isc.isAn.Array(screenName)) screenName = [screenName];
        if (screenName.length <= 0) {
            this.logWarn("No screen names passed in.");
            this.fireCallback(callback, "data", [null]);
            return;
        }
        if (!globals) globals = [];
        if (!isc.isAn.Array(globals)) globals = [globals];
        var request = {};
        if (requestProperties) isc.addProperties(request, requestProperties);
        if (request.params == null) request.params = {};
        request.params.screenName = screenName.join(",");
        if (locale) {
            isc.addProperties(request.params, {locale: locale});
        }
        if (!request.actionURL) request.actionURL = this.screenLoaderURL;
        request.useSimpleHttp = true;

        var _this = this;
        request.callback = function (rpcResponse, data, rpcRequest) {
            _this._makeScreen(rpcResponse, data, rpcRequest, callback, globals);
        };
        this.sendRequest(request);
    },

    //> @classMethod RPCManager.createScreen()
    // Creates a screen previously cached by a call to +link{cacheScreens()}.
    // <p>
    // As with +link{loadScreen()}, the default behavior is to prevent any global widget IDs from
    // being established, the returned Canvas will be the outermost component of the screen,
    // and that Canvas will provide access to other widgets in the screen via +link{canvas.getByLocalId(),getByLocalId()}
    // <p>
    // Alternatively, as with +link{loadScreen()}, a list of IDs that should be allowed to become
    // globals can be passed, allowing those widgets to be retrieved via a call to
    // +link{Canvas.getById()} after the screen has been created.
    // <p>
    // If you do not pass <code>globals</code> and avoid depending on global IDs within the screen
    // definition itself (for example, by embedding JavaScript event handlers in the screen definition
    // that use global IDs), you can create the same screen multiple times.
    //
    // @param screenName (String) name of the screen to create
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @return (Canvas) last top-level widget in the screen definition
    //
    // @visibility external
    //<
    createScreen : function(screenName, globals) {
        if (!screenName) {
            this.logWarn("No screen names passed in.");
            return null;
        }

        if (!globals) globals = [];

        if (!isc.isAn.Array(globals)) globals = [globals];

        var _this = this;
        var data = _this._cachedScreens[screenName];

        if (!data) { return null;}

        return this._makeScreen(null, data, null, null, globals);
    },

    _makeScreen : function(rpcResponse, data, rpcRequest, callback, globals) {
        if (!rpcRequest) { rpcRequest = {} };

        var origAutoDraw = isc.Canvas.getInstanceProperty("autoDraw"),
            suppressAutoDraw = rpcRequest.suppressAutoDraw == null ? true : rpcRequest.suppressAutoDraw;
        if (suppressAutoDraw) isc.Canvas.setInstanceProperty("autoDraw", false);

        var _this = this;
        if (globals.length == 1 && globals[0] == _this.ALL_GLOBALS) {
            var result = isc.Class.globalEvalWithCapture(data, function (globals, error) {
                if (error != null) isc.Log._reportJSError(error, null, null, null,
                                                  "Error when executing loaded screen");
                // get the top level component
                var _screen = isc.Canvas._getTopLevelWidget(globals);
                // globalEvalWithCapture does not set suppressedGlobals in the fired callback. Should we set it to empty?
                var suppressedGlobals = {};

                // restore autoDraw here, to be on safe side if error occurs in the callback
                if (suppressAutoDraw) isc.Canvas.setInstanceProperty("autoDraw", origAutoDraw);

                _this.fireCallback(callback, "screen,rpcResponse,suppressedGlobals",
                    [_screen, rpcResponse, suppressedGlobals]);
            }, null, false);

            var _screen = isc.Canvas._getTopLevelWidget(result.globals);
            return _screen;
        } else {
            var result =  isc.Class.globalEvalAndRestore(data, globals, function (globals, error, suppressedGlobals) {
                if (error != null) isc.Log._reportJSError(error, null, null, null,
                                                  "Error when executing loaded screen");
                // get top level view
                var _screen = isc.Canvas._getTopLevelWidget(globals);

                // restore autoDraw here, to be on safe side if error occurs in the callback
                if (suppressAutoDraw) isc.Canvas.setInstanceProperty("autoDraw", origAutoDraw);

                // filter supressed globals so only allowed items get through
                // this is required to prevent exception on load when there are other elements,
                // like DataSource definitions in the ui.xml file.
                var tmp = {};
                var keys = isc.getKeys(suppressedGlobals)
                for (var i = 0; i < keys.length; i++) {
                    var global = keys[i];
                    var obj = window[global]; // globals are IDs, dereference

                    if (obj && (isc.isA.Canvas(obj) || isc.isA.FormItem(obj)) ) {
                        tmp[global] = obj;
                    }
                }

                suppressedGlobals = tmp;

                _this.fireCallback(callback, "screen,rpcResponse,suppressedGlobals",
                    [_screen, rpcResponse, suppressedGlobals]);
            }, null, false, true);

            // get top level view
            var _screen = isc.Canvas._getTopLevelWidget(result.globals);

            return _screen;
        }
    },

    //> @classMethod RPCManager.cacheScreens()
    // Loads the definitions of a set of screens saved in +link{group:componentXML,Component XML}
    // format, using the +link{group:servletDetails,ScreenLoaderServlet}.
    // <p>
    // Unlike +link{loadScreen()}, <code>cacheScreens()</code> does not cause any UI components to be
    // created or drawn, it just loads the definitions of the screens.  This allows a subsequent,
    // synchronous call to +link{createScreen()} to create the actual screen, rather than
    // contacting the <code>ScreenLoader</code> servlet and showing a loading message.
    // <p>
    // See +link{loadScreen()} for the meaning of the <code>locale</code> parameter.
    // <p>
    // Calling <code>cacheScreens</code> twice with the same screenName will re-load the definition of
    // that screen from the server such that subsequent calls to <code>createScreen()</code> will use
    // the new definition.
    //
    // @param screenName (Array of String) name of the screens to cache
    // @param callback (Function) callback for notification of screens being successfully cached
    // @param [locale] (String) The name of a locale to use for resolving i18n tags in the
    //                         component XML of the screen
    // @param [requestProperties] (RPCRequest Properties) optional properties for the request
    //
    // @visibility external
    //<
    cacheScreens : function(screenName, callback, locale, requestProperties) {
        if (!screenName) {
            this.logWarn("No screen names passed in.");
            this.fireCallback(callback, "data", [null]);
            return;
        }
        // Note: this logic will handle multiple screens, but this is intentionally not
        // documented and not supported.  The right way to load multiple screens is to use
        // RPCManager.cacheScreens().
        if (!isc.isAn.Array(screenName)) screenName = [screenName];
        if (screenName.length <= 0) {
            this.logWarn("No screen names passed in.");
            this.fireCallback(callback, "data", [null]);
            return;
        }

        var request = {};
        if (requestProperties) isc.addProperties(request, requestProperties);
        request.params = {screenName: screenName.join(",")};
        if (locale) {
            isc.addProperties(request.params, {locale: locale});
        }
        isc.addProperties(request.params, {structuredResponse:true});

        if (!request.actionURL) request.actionURL = this.screenLoaderURL;
        request.useSimpleHttp = true;
        var _this = this;

        request.callback = function (rpcResponse, data, rpcRequest) {

            isc.Class.globalEvalWithCapture(data, function (globals, error) {
                if (error != null) isc.Log._reportJSError(error, null, null, null,
                                                  "Error when executing cache screen");
                if (!_this._cachedScreens) {
                    _this._cachedScreens = {};
                }

                var json = isc.Class.evaluate(data);
                for (var i=0;i<json.length;i++) {
                    if (_this._cachedScreens[json[i].screenName]) {
                        _this.logWarn("Screen " + json[i].screenName + " is already cached. Replacing.");
                    }
                    _this._cachedScreens[json[i].screenName] = json[i].source;
                }

                _this.fireCallback(callback, "data,rpcResponse", [json, rpcResponse]);
            }, null, false);
        };

        this.sendRequest(request);
    },

    //> @classMethod RPCManager.requestsArePending()
    // Returns whether there are any pending RPC requests.
    // <P>
    // @return (Boolean) true if one or more RPC requests are pending, false otherwise.
    // @visibility external
    //<
    requestsArePending : function () {
        return this.pendingRpcs != 0;
    }

});
// patch RPCManager logging to truncate anything abot maxLogMessageLength with a warning and
// allow a second category to enable full logs.  We do this to prevent accidentall logging of
// large responses such as roundripping a WSDL file or toJS.
isc.RPCManager.rpc_logMessage = isc.RPCManager.logMessage;
isc.RPCManager.logMessage = function (priority, message, category, timestamp) {
    if (this.logIsEnabledFor(priority, category)) {
        if (isc.isA.String(message) && message.length > this.maxLogMessageLength
            && !this.logIsEnabledFor(priority, "RPCManagerResponse"))
        {
            var delta = message.length - this.maxLogMessageLength;
            message = message.substring(0, this.maxLogMessageLength)
                +"\n...("+delta+" bytes truncated).  Enable RPCManagerResponse log at same threshold to see full message."
        }
    }
    this.rpc_logMessage(priority, message, category, timestamp);
};

//>    @class    InstantDataApp
// An InstantDataApp provides the ability to send data to an ISC server and retrieve results, and
// also to determine what users are authorized to perform what operations.
// <br><br>
// All server contact in the ISC system goes through InstantDataApp.  In higher-level APIs, such as
// the component databinding methods, the InstantDataApp is not directly visible because of the
// concept of the "default application", however, all such methods boil down to the
// <code>performOperation()</code> call, which allows arbitrary data to be sent and retrieved.
// <br><br>
// An InstantDataApp is created by loading an ISC Application File (.app.xml), which is read by both
// the ISC server and ISC client system.  The Application File allows you to specify the server-side
// Class which should handle operations submitted by the client, so that you can write custom
// operations.
//
// @see classMethod:InstantDataApp.getDefaultApplication()
// @group operations
// @treeLocation Client Reference/Data Binding
// @visibility ida
//<
isc.addGlobal("InstantDataApp", isc.RPCManager);
isc.isA.InstantDataApp = isc.isA.RPCManager; // make isA.InstantDataApp(app) work

isc.InstantDataApp.addClassMethods({

    // Operations
    // ----------------------------------------------------------------------------------------

    // legacy compatibility for performOperation(): ensures a client-side operation definition
    // exists for the operation name, because performOperation() needs one to exist, since for
    // DataSource operations the parameters to performOperation are insufficient to give
    // dataSource name and operationType
    addDefaultOperation : function (context, dataSource, operationType) {
        if (!context) context = {};

        context.operation = isc.DataSource.makeDefaultOperation(dataSource, operationType,
                                                                context.operation);

        return context;
    },


    // Default Application handling
    // --------------------------------------------------------------------------------------------
    setDefaultApplication : function (defaultApplication) {
        isc.InstantDataApp.defaultApplication = defaultApplication;


    },

    //> @classMethod RPCManager.getDefaultApplication()
    // Get the default application.
    // <br><br>
    // The default application is the first RPCManager created in a given page, or if no
    // RPCManagers are created, an automatically generated RPCManager with the ID
    // "builtinApplication".
    // <br><br>
    // The default application will be used any time an operation needs to be performed and no
    // application has been explicitly specified.  For example, a databound ListGrid is asked to
    // fetchData() and no application is passed.
    // <br><br>
    // The special automatically-generated "builtinApplication" is for rapid prototyping; it allows
    // you to perform the built-in operations against any DataSource, so that you can prototype a
    // complete client-server application without writing any server code and without writing an ISC
    // Application File (.app.xml file).
    //
    // @return (RPCManager) the default application
    // @visibility internal
    //<
    getDefaultApplication : function () {
        // If the default application has never been created or has been destroyed we need to
        // create a new one.
        if (this.defaultApplication == null) {
            // create a barebones default application, which will automatically register itself
            // as the default application
            this.create({
                ID:"builtinApplication",
                dataSources:[],
                operations:{},
                // Ensure that should the default app get destroy()d, our pointer to it gets
                // cleared
                pointersToThis:[{object:this, property:"defaultApplication"}]
            });
        }
        return this.defaultApplication;
    },
    app : function () { return this.getDefaultApplication(); }
});

isc.InstantDataApp.addMethods({

    init : function () {
        // Create a global ID for this application so we can refer to it in the global scope.
        // If the app already has an ID property, this will be used as its global ID.
        if (this.ID != "builtinApplication") isc.ClassFactory.addGlobalID(this);

        // if there's no current default application, or the auto-generated
        // "builtinApplication" has been set as the default application, replace it
        if (isc.rpc.defaultApplication == null ||
            isc.rpc.defaultApplication.getID() == "builtinApplication")
        {
            isc.rpc.setDefaultApplication(this);
        }
    }

});


// hooks into RPCManager and provides notifications of queue status
isc.defineInterface("IRPCStatusListener").addInterfaceProperties({

initInterface : function () {
    this.observe(isc.RPCManager._transactions, "dataChanged", "observer.transactionsChanged()");
},

destroyInterface : function () {
    this.ignore(isc.RPCManager._transactions, "dataChanged");
},

transactionsChanged : function () {

},

getActiveTransactions : function () {
    var activeTransactions = [];
    var transactions = isc.RPCManager.getTransactions();

    for (var i = 0; i < transactions.length; i++) {
        var transaction = transactions[i];
        // XXX also exclude transaction._clearOnTimeout?  But the response from those would be processed...
        if (transaction && !transaction.cleared && transaction.operations) {
            var abortableTransaction = true;
            for (var j = 0; j < transaction.operations.length; j++) {
                var operation = transaction.operations[j];
                if (operation.downloadResult || operation._returnStreamFileURL) {
                    abortableTransaction = false;
                    break;
                }
            }
            if (abortableTransaction) activeTransactions.add(transaction);
        }
    }
    return activeTransactions;
},

haveActiveTransactions : function () {
    return this.getActiveTransactions().length > 0;
}

});


isc.defineInterface("IRPCUnmaskedTarget").addInterfaceProperties({

initInterface : function () {
    isc.RPCManager.registerUnmaskedTarget(this);
},

destroyInterface : function () {
    isc.RPCManager.unregisterUnmaskedTarget(this);
}

});



isc.defineClass("UnmaskedTargetLandingPad", "Canvas").addProperties({

overflow: "visible",
height: 1,
width: 1,
observationHooks: ["draw", "clear", "moved", "parentMoved", "parentResized", "parentVisibilityChanged"],

initWidget : function () {
    this.Super("initWidget", arguments);
    for (var i = 0; i < this.observationHooks.length; i++) {
        this.observe(this, this.observationHooks[i], "observer.manageUnmaskedTarget()");
    }
    this.setUnmaskedTarget(this.unmaskedTarget);
},

setUnmaskedTarget : function (unmaskedTarget) {
    this.unmaskedTarget = unmaskedTarget;
    if (this.unmaskedTarget) {
        this.observe(this.unmaskedTarget, "resized", "observer.manageUnmaskedTarget()");
    }
},

manageUnmaskedTarget : function () {
    if (!this.unmaskedTarget) return;

    if (this.isVisible() && this.isDrawn()) {
        // Note update our width/height first before repositioning the unmaskedTarget because
        // we are likely to be contained whereas the unmaskedTarget is not, so when it changes
        // size it overflows to the right whereas we may need to overflow to the left
        if (this.getVisibleWidth() < this.unmaskedTarget.getVisibleWidth()) this.setWidth(this.unmaskedTarget.getVisibleWidth());
        if (this.getVisibleHeight() < this.unmaskedTarget.getVisibleHeight()) this.setHeight(this.unmaskedTarget.getVisibleHeight());

        // record page-level coordinates before reparent
        var left = this.getPageLeft(),
            top = this.getPageTop();

        this.unmaskedTarget.moveTo(left, top);
        this.unmaskedTarget.bringToFront();
        this.unmaskedTarget.show();
    } else {
        this.unmaskedTarget.hide();
    }
},

destroy : function () {
    for (var i = 0; i < this.obsevationHooks.length; i++) {
        this.ignore(this, this.observationHooks[i]);
    }
    // for the resized event, watch the unmaskedTarget and apply the size change to ourselves.
    // The unmaskedTarget is the one that can overflow due to e.g. prompt changes whereas the
    // placeholder (this class) doesn't have any content and just needs to expand to fill space
    // under the unmaskedTarget
    if (this.unmaskedTarget) this.ignore(this.unmaskedTarget, "resized");

    this.Super("destroy", arguments);
}

});


isc.defineClass("RPCStatusControl", "UnmaskedTargetLandingPad").addProperties({

statusIndicatorDefaults: {
    _constructor: "RPCStatusMenuButton"
},

initWidget : function () {
    this.Super("initWidget", arguments);

    this.statusIndicator = this.createAutoChild("statusIndicator", {
        canCancelQueue: this.canCancelQueue
    });
    this.setUnmaskedTarget(this.statusIndicator);
}

});


if (isc.MenuButton) {

isc.defineClass("RPCStatusMenuButton", "MenuButton", ["IRPCStatusListener", "IRPCUnmaskedTarget"]).addProperties({

width: 40,
height: 20,

defaultPrompt: "Idle",

progressImage: "[SKINIMG]loadingSmall.gif",
progressImageWidth: 16,
progressImageHeight: 16,

noProgressImage: "[SKINIMG]loadingSmallFrozen.gif",
noProgressImageWidth: 16,
noProgressImageHeight: 16,

canCancelQueue: false,
confirmCancelQueue: true,
cancelQueueConfirmationMessage: "Cancelling a request may have unintended consequences. "
                                + " You should only do this if you requested data that is taking"
                                + " too long to load, but not if you are saving data.<br><br>"
                                + " Are you sure you want to cancel this request?",

cancelQueue : function () {
    var activeTransactions = this.getActiveTransactions();
    for (var i = 0; i < activeTransactions.length; i++) {
        this.logWarn("cancel queue: " + i + " with value: " + isc.echoFull(activeTransactions[i].transactionNum));
        isc.RPCManager.cancelQueue(activeTransactions[i]);
    }
},

initWidget : function () {
    this.Super("initWidget", arguments);

    var menuItems = [];
    menuItems.add({title: isc.RPCManager.defaultPrompt, isPrompt: true, enabled: false});
    if (this.canCancelQueue) {
        var _this = this;
        menuItems.add({title: "Cancel all requests", icon: "[SKINIMG]actions/close.png", click : function () {
            if (_this.confirmCancelQueue) {
                isc.confirm(_this.cancelQueueConfirmationMessage, function (yes) {
                    if (yes) _this.cancelQueue();
                });
            } else {
                _this.cancelQueue();
            }
        }});
    }
    this.menu = isc.Menu.create({
        data: menuItems
    });
    this.menuButton = this.createAutoChild("menuButton", {
        menu: this.menu
    });
    this.updateState();

    this.addAutoChildren(this.autoChildren);
},

updateState : function () {
    var activeTransactions = this.getActiveTransactions();

    var lastTransaction = isc.RPCManager.getTransaction(activeTransactions[activeTransactions.length-1]);

    var title = "";
    var promptMenuItem = this.menu.data.find("isPrompt", true);
    if (lastTransaction) {
        title += isc.Canvas.getImgHTML(this.progressImage, this.progresImageWidth, this.progressImageHeight);

        if (promptMenuItem) this.menu.setItemTitle(promptMenuItem, lastTransaction.prompt);

        this.setDisabled(false);
    } else {
        title += isc.Canvas.getImgHTML(this.noProgressImage, this.noProgresImageWidth, this.noProgressImageHeight);

        if (promptMenuItem) this.menu.setItemTitle(promptMenuItem, this.defaultPrompt);

        this.setDisabled(true);
    }
    this.setTitle(title);
},

transactionsChanged : function () {
    this.updateState();
}

});
}

//> @groupDef operations
// SmartClient Operations are dynamic, transparent communications made from the client-side
// SmartClient system running in the browser, to the server-side SmartClient system running in
// a servlet engine, or to other non-SmartClient servers available via HTTP. Operations are
// used to load new data or new behavior into a running SmartClient application. Operations are
// also used to save data entered by users, and in general, to get the result of any process
// which must be run on the server for security reasons.
// <br><br>
//
// <b>RPC Operations</b>
// <br><br>
// RPC Operations are low-level communications that send and retrieve arbitrary data.  RPC
// Operations are supported by the +link{class:RPCManager} class, which when used with the
// SmartClient server, provides Java to JavaScript +link{rpcRequest.data,2-way translation} of
// basic data structures.  The RPCManager also provides a mechanism for client-side code to be
// invoked when an operation completes (called a "callback").  RPC Operations are intended for
// unstructured data; data that is ultimately destined for display in SmartClient components
// will generally come from DataSource operations.
// <br><br>
//
// <b>DataSource Operations and DataBound Components</b>
// <br><br>
// A +link{group:dataSourceOperations,DataSource Operation} is an operation that acts on a
// DataSource, performing one of the basic actions that makes sense on a set of similar
// records: "fetch", "add", "update" or "remove".  Unlike RPC operations, DataSource operations
// have specific request data and response data, for example, in the "fetch" DataSource
// operation, the request data is expected to be search criteria, and the response data is
// expected to be a list of matching DataSource records.  Although DataSource operations can be
// invoked manually from the client, they are generally automatically invoked by DataBound
// components.
// <br><br>
// DataBound Components are components that understand DataSources.  Databound components
// configured with a DataSource are able to offer complete user interactions without further
// configuration (extensive customization is also supported).
// <br><br>
// For example, given a DataSource, the ListGrid component supports a sophisticated inline
// editing interaction, complete with automatically chosen editors like date pickers for dates,
// type-aware validation, saving, and error reporting.
// <br><br>
// A DataBound component supporting an interaction such as inline editing will automatically
// submit DataSource operations to the server at appropriate times.
// <br><br>
//
// <b>DataSource Operation Integration</b>
// <br><br>
// Integrating DataSource operations with an existing system is best approached by implementing
// the the 4 basic DataSource operations in terms of your existing object model or data store.
// With these 4 operations implemented, the entire range of user interactions supported by
// SmartClient +link{dataBoundComponent,databinding-capable components} becomes applicable to
// your server.  At that point authentication, authorization and other business rules can be
// layered on top.
// <br><br>
//
// <b>Built-in SQL Connectivity</b>
// <br><br>
// The SmartClient Server comes with a built-in +link{group:sqlDataSource,SQLDataSource}
// which can be used without any server-side code needing to be written.  In contrast,
// any operation which uses custom server-side code is called a "Custom Operation".
// <br><br>
// Generally it makes sense to prototype an application using Built-in DataSource Operations,
// then on the backend, create Custom DataSource Operations to retrieve data from the data
// store you will use in production (though don't rule out using the SQL DataSource in
// production - see +link{group:sqlVsJPA,this discussion} of the advantages of doing so}.
// As you switch from using Built-in DataSources to Custom Operations, no client-side code
// changes will be required, because the client cares only about the DataSource definition,
// not the data store which the data is ultimately retrieved from.
// <br><br>
//
// <b>Data Managers: ResultSet and ResultTree</b>
// <br><br>
// Data Managers manage datasets retrieved from DataSources.  Data Managers are automatically
// created by DataBound components, but can be created directly when more control is needed.
// <br><br>
// Data Managers provide load-on-demand for datasets too large to be loaded on the client,
// automatically invoking DataSource operations as necessary to retrieve data as it is
// requested, and optionally fetching ahead to anticipate further requests.   Data Managers
// will automatically perform actions locally when it is possible, for example, a sort
// action can be performed locally with a complete cache.  Data Managers also automatically
// manage the consistency of the client-side cache, observing update operations performed
// against DataSources and integrating updated rows automatically.
// <br><br>
//
//
// @see class:RPCManager for RPC Operations
// @see interface:DataBoundComponent for information on DataBound Components
// @see group:dataSourceOperations for more information on DataSource Operations
// @see group:clientServerIntegration for information on integrating DataSource Operations with existing servers
// @see method:DataSource.fetchData() for manually invoked DataSource operations
// @see class:ResultSet for managing lists of records
// @see class:ResultTree for managing trees of records
//
// @title Operations Overview
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

//> @groupDef dataSourceOperations
// A DataSource Operation is a type of +link{group:operations,operation} that acts on the set
// of stored objects represented by a +link{DataSource}, performing one of the basic actions
// that makes sense on a set of similar records: "fetch", "add", "update" or "remove".  There
// is also a fifth DataSource Operation, "custom", which is intended for arbitrary server
// operations that are more complex than a fetch of some records, or an update to a single
// record.
// <P>
// Each DataSource operation has specific request and response data, for example, in the
// "fetch" DataSource operation, the request data is expected to be search criteria, and the
// response data is expected to be a list of matching DataSource records.  Listed below are the
// request data and response data for each DataSource operation type, and what they mean.
// <P>
// DataSource records are represented on the client by a JavaScript Object,
// where each property in the Object maps a DataSource field name to the field value - hence
// the DataSource operations below are in essence a way of exchanging records from client to
// server and back.
// <P>
// If you are using +link{group:serverDataIntegration,server-side data integration} with the
// SmartClient Java server, see the +docTreeLink{javaServerReference,Java Server Reference} for
// information about how DataSource Requests arrive on the server (specifically
// com.isomorphic.datasource.DSRequest) and how to provide responses
// (specifically com.isomorphic.datasource.DSResponse.setData()).
// <P>
// If you are using +link{group:clientDataIntegration,client-side data integration} to directly
// consume services that use XML, JSON or other formats, see the "Editing and Saving" section
// of the +link{group:clientDataIntegration,client-side data integration} topic.
// <P>
//
// <b>fetch</b>
// <ul>
// <li>Request data: filter criteria, as an Object
// <li>Response data: matching records, as an Array of Objects
// </ul>
//
// <b>add</b>
// <ul>
// <li>Request data: new record, as an Object
// <li>Response data: new record as stored, as an Object
// </ul>
//
// <b>update</b>
// <ul>
// <li>Request data: primary keys of record to update, and new values (or just complete updated
// record), as an Object
// <li>Response data: new record as stored, as an Object
// </ul>
//
// <b>remove</b>
// <ul>
// <li>Request data: primary keys of record to delete, as an Object
// <li>Response data: minimally the primary keys of deleted record (can be complete record), as
// an Object
// </ul>
//
// <b>custom</b>
// <ul>
// <li>Request data: whatever the custom operation requires
// <li>Response data: custom operations can return whatever they like, including nothing.
// Custom operations are like RPC calls in this respect - the exchanged data is unstructured,
// so it is up to you to make sure the client and server agree.  Note also that, because of
// this unstructured data exchange, cache synchronization does not work with custom operations.
// </ul>
//
// @title DataSource Operations
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

//> @groupDef clientServerIntegration
//
// Like client-server desktop applications, SmartClient browser-based applications interact
// with remote data and services via background communication channels. Background requests
// retrieve chunks of data rather than new HTML pages, and update your visual components in
// place rather than rebuilding the entire user interface.
// <P>
// <b>DataSources</b>
// <p>
// First you must create +link{class:DataSource,DataSources} that describe the objects from
// your object model that will be loaded or manipulated within your application.  All of
// SmartClient's most powerful functionality builds on the concept of a DataSource, and because
// of SmartClient's databinding framework (see +link{DataBoundComponent}), it's as easy to
// create a DataSource that can configure an unlimited number of components as it is to
// configure a single component.
// <P>
// For background information on how to create DataSources, +link{DataBoundComponent, bind}
// components to DataSources and initiate +link{DSRequest}s, please see the <em>Data
// Binding</em> chapter of the <em>SmartClient Quickstart Guide</em>.
// <P>
// <b>Data Integration</b>
// <P>
// DataSources provide a data-provider agnostic API to SmartClient Visual Components that
// allow them to perform the 4 CRUD operations (<b>C</b>reate, <b>R</b>etrieve,
// <b>U</b>pdate, <b>D</b>elete).  By "agnostic" we mean that the implementation details -
// the nuts and bolts of how a given DataSource actually retrieves or updates data - are
// unknown to bound SmartClient components.  One effect of this is that DataSources are
// "pluggable": they can be replaced without affecting the User Interface.
// <p>
// When a visual component, or your own custom code, performs a CRUD operation on a DataSource,
// the DataSource creates a +link{DSRequest} (DataSource Request) representing the operation.
// "Data Integration" is the process of fulfilling that DSRequest by creating a corresponding
// +link{DSResponse} (DataSource Response), by using a variety of possible approaches to
// connect to the ultimate data provider.
// <p>
// There are two main approaches to integrating DataSources with your server technology:
// <ul>
// <li><b>Server-side integration</b>: DataSource requests from the browser arrive as Java
// Objects on the server. You deliver responses to the browser by returning Java Objects. The
// various server-side integration possibilities are discussed later in this article.</li>
// <li>+link{clientDataIntegration,Client-side integration}: DataSource requests arrive as
// simple HTTP requests which your server code receives directly (in Java, you use the
// Servlet API or .jsps to handle the requests). Responses are sent as XML or JSON which you
// directly generate.</li>
// </ul>
// The possible approaches are summarized in the diagram below. Paths 2, 3 and 4 are
// client-side integration approaches, and path 1 includes all server-side integration
// approaches.
// <p>
// <img src="skin/ClientServerIntegration.png" width="866px" height="495px">
// <p>
// SmartClient supports, out of the box, codeless connectivity to various kinds of common data
// providers, including SQL and Hibernate.  SmartClient also provides functionality and tools
// for accelerated integration with broad categories of data providers, such as Java
// Object-based persistence mechanisms (JPA, EJB, Ibatis, in-house written systems), and REST
// and WSDL web services in XML or JSON formats.  Ultimately, a DataSource can be connected to
// anything that is accessible via HTTP or HTTPS, and also to in-browser persistence engines
// such as +externalLink{http://gears.google.com,Google Gears}.
// <p>
// <b>Choosing a Data Integration Approach</b><p>
// This section aims to help you decide which of the many possible data integration approaches
// is best for your particular circumstances.  The recommendations given here will guide you
// to the approach that involves the least effort.<p>
// <img src="skin/dataIntegrationFlowchart.png" width="640px" height="300px">
// <p>
// <ul>
// <li>If you have a Java server:</li>
// <ul>
//   <li>If your ultimate storage is a SQL database:</li>
//     <ul>
//       <li>Use the SQLDataSource unless you have a very large amount of pre-existing
//           JPA or Hibernate code - small amounts of business logic can be easily migrated.
//           Be sure to read the overview of +link{group:sqlVsJPA,SQLDataSource vs JPA/Hibernate}
//           in order to understand the large benefits the SQLDataSource provides</li>
//       <li>Derive DataSource definitions from existing tables or Hibernate mappings using the
//           +link{dataSource.autoDeriveSchema,autoDeriveSchema} feature, or from Java Beans
//           via the +link{dataSource.schemaBean,schemaBean} feature.
//           Or, use the +link{adminConsole,Admin Console} to generate tables from DataSource
//           definitions you create by hand</li>
//     </ul>
//   <li>If your ultimate storage is not a SQL database:</li>
//     <ul>
//       <li>If your persistence is based on Java Beans, use the
//           +link{dataSource.schemaBean,schemaBean} feature to derive DataSource definitions from
//            any Java bean</li>
//       <li>write a +link{group:writeCustomDataSource,custom DataSource} that provides the
//           CRUD operations you want to support.</li>
//     </ul>
//   <li>Whether or not your storage is SQL, add business logic either declaratively in the
//       DataSource definition, via +link{dmiOverview,DMI}, or any combination of the two:
//     <ul>
//       <li>The &lt;criteria&gt; and &lt;values&gt; properties of an +link{class:OperationBinding}
//           allow you to dynamically set data values at transaction-processing time, using
//           built-in +link{group:velocitySupport,Velocity support}</li>
//       <li>Override the <code>validate()</code> method of the DataSource to provide extra
//           custom validations - just call <code>super</code> to obtain the list of errors
//           derived from SmartClient validations, then add to that list as required with your
//           own custom code</li>
//       <li>Override the <code>execute()</code> method of the DataSource to add extra processing
//           either before or after the SmartClient processing</li>
//       <li>Use +link{attr:DSRequestModifier.value,Transaction Chaining} to dynamically set
//           data values according to the results of earlier transactions</li>
//       <li>For SQL DataSources, use +link{group:customQuerying,SQL Templating} to change,
//           add to or even completely replace the SQL sent to the database, and to implement
//           special query requirements</li>
//       <li>For JPA DataSources, use +link{attr:OperationBinding.customJQL,custom JQL queries}
//           to implement special query requirements</li>
//       <li>For Hibernate DataSources, use +link{attr:OperationBinding.customHQL,custom HQL queries}
//           to implement special query requirements</li>
//      </ul>
//      Read more about the server-side request processing flow and how to customize it in
//      +link{group:serverDataIntegration,the server integration overview}.
// </ul>
// </ul>
// <ul>
// <li>If you do not have a Java server:</li>
//   <ul><li>If you are not obliged to use a pre-existing network protocol, use the
//           +link{class:RestDataSource}</li>
//       <li>Otherwise, use +link{clientDataIntegration,client-side data integration} features
//           to create a custom client-side DataSource that adapts the DataSource protocol to
//           your existing services</li>
//   </ul>
// </ul>
// <p><br>
// <b>RPCs: Unstructured Server Communication</b>
// <P>
// SmartClient also supports "unstructured" client-server operations.  These
// +link{RPCRequest}s (Remote Procedure Call Requests) are a low-level, very flexible
// mechanism for custom client-server communications.  In an nutshell, RPCRequests:
// <ul>
// <li> may contain arbitrary data
// <li> are always initiated by custom code (a call to +link{RPCManager.send()}), and have
// their responses handled by custom code (the callback passed to <code>send()</code>)
// </ul>
// <P>
// RPCRequests are relatively rare.  Most client-server communications are better done in a
// structured fashion using a +link{DSRequest} (DataSource Request).  Note that <em>any</em>
// RPCRequest can alternatively be framed as a +link{method:dataSource.fetchData,DataSource fetch};
// depending on the circumstances, this may be more convenient.
// <P>
// See the +link{RPCManager} documentation for further information on RPCRequests.
//
// @title Client-Server Integration
// @treeLocation Concepts
// @visibility external
//<

//> @groupDef writeCustomDataSource
// Out of the box, and with no code to write, SmartClient supports SQL, JPA and Hibernate for
// persistence, which includes EJB 3.0, EclipseLink and other Java persistence systems accessible
// via JPA.  For other Java-based persistence systems, such as legacy EJBs or systems
// proprietary to your company, you write a custom DataSource class in Java.  In most cases, it
// is possible to write a single, generic DataSource class that provides access to all data
// that is a available from a given persistence mechanism; for example, a single DataSource
// class can typically be written for accessing all data accessible via legacy EJB.
// <p>
// Note that a majority of the features of the SmartClient Server framework apply even when
// using your own persistence mechanism.  As with the features supported by SmartClient's
// browser-based visual components, SmartClient's server-side features rely only on the
// concept of a DataSource and not on the details of the ultimate persistence mechanism.  Hence
// they are usable with a custom DataSource regardless of the final data provider.
// <p>
// We provide a complete working example of a custom DataSource in the SmartClient Feature
// Explorer; you can see it in action +explorerExample{ormDataSource,here}.  This example
// "ormDataSource" is an adaptor for Hibernate which supports the 4 CRUD operations,
// data paging, server-side sort and filter, and which participates correctly in
// +link{ResultSet,cache synchronization}.  The code required is minimal, and the approaches
// taken generalize to any ORM system.  Studying the Java source code for this DataSource -
// which is available in the "ORMDataSource.java" tab in the example linked to above - is the
// best way to get a start on implementing your own custom DataSource.
// <p>
// <ul>
// <li><code>ORMDataSource</code> extends <code>BasicDataSource</code>.
// <li><code>ORMDataSource</code> is primarily an implementation of four key methods:
//     <code>executeFetch</code>, <code>executeAdd</code>, <code>executeUpdate</code> and
//     <code>executeRemove</code>.  All the logic related to the actual CRUD data operation
//     takes place in one of these methods.  This is the recommended approach.</li>
// <li>The class also implements the <code>execute</code> method.  This is an override of the
//     method that is actually called by the framework, and as such is an appropriate place to
//     set up shared objects that will be used in more than one CRUD operation, and to perform
//     shared pre- and post-processing.  As you can see, the example is setting up a Hibernate
//     session and transaction, and then calling <code>super.execute</code> - this calls back
//     into the framework and ultimately leads to the appropriate data operation method being
//     called.</li>
// <li>Note how each of the <code>executeXxx</code> methods conforms to the
//     +link{dataSourceOperations,DataSource protocol}.  To take <code>executeFetch</code> as
//     an example, note how it:
//     <ul><li>Retrieves the criteria for the fetch from the supplied <code>DSRequest</code></li>
//         <li>Implements logic to obey the <code>startRow</code>, <code>endRow</code> and
//             <code>batchSize</code> values.  This is only necessary for a DataSource that
//             intends to support automatic data paging.</li>
//         <li>Retrieves <code>sortByFields</code> from the supplied <code>DSrequest</code>,
//             and uses that value to change the order of the resultset.  This is only
//             necessary for a DataSource that intends to support server-side sorting.</li>
//         <li>Populates <code>startRow</code>, <code>endRow</code> and <code>totalRows</code>
//             on the <code>DSResponse</code>.</li>
//         <li>Populates the <code>DSResponse</code>'s <code>data</code> member with the list of
//             objects retrieved by the Hibernate call.</li>
//     </ul><br>
//     These are the only parts of this method that are of significance as far as SmartClient
//     is concerned - the rest of the method is concerned with communicating with the
//     data provider, which is of no interest to SmartClient as long as the method conforms to
//     the DataSource protocol for a "fetch" operation.</li>
// </ul>
// <p><br>
// <b>The DataSource descriptor</b>
// <p>
// Once your custom DataSource is implemented, you need to to create a descriptor for each
// instance of the DataSource.  As noted above, it is generally possible to write one custom
// DataSource class that is capable of handling all data access for a particular persistence
// mechanism.  DataSource descriptors, on the other hand, are written per entity.
// <p>
// A DataSource descriptor is an XML file with the special suffix <code>.ds.xml</code>.  The
// descriptor for a custom DataSource is, for the most part, identical to the descriptor for
// a built-in DataSource: it is the central place where you describe the DataSource instance
// to the system - its fields, validations, security constraints, special data operations,
// transaction chaining expressions and so on (see the +link{class:DataSource,DataSource docs}
// for full details).
// <p>
// One property that is always required for a custom DataSource is
// +link{attr:DataSource.serverConstructor,serverConstructor}.  This fully-qualified class
// name tells SmartClient what to instantiate when data operations for this DataSource arrive
// on the server - in other words, it is how you tell SmartClient to use your custom class.
// In the +explorerExample{ormDataSource,ORM DataSource example}, on the
// <code>ormDataSource_country</code> tab, you will see how we use this property to tie the
// <code>ormDataSource_country</code> DataSource <em>instance</em> to the
// <code>ormDataSource</code> DataSource <em>implementation</em>.
// <p>
// Finally, if your data model is based on Javabeans, or on POJOs that broadly follow the
// Javabean conventions (basically, if they have private state variables accessible via public
// getters and setters), SmartClient can automatically generate basic DataSource definitions
// for your beans that will only need minimal change (ie, specifying a
// <code>serverConstructor</code>) to be fully operational.  Both the
// +explorerExample{javabeanWizard,Visual Builder Javabean Wizard} and the Batch DataSource
// Generator can create DataSource descriptors from existing beans.
// <p>
// <b>Server framework features relevant to custom DataSources</b>
// <P>
// The vast majority of the SmartClient Server framework's key features are not specific to the
// built-in SQL and Hibernate connectors, and still apply even when using a custom persistence
// mechanism.  See +link{group:featuresCustomPersistence,this overview} of which features apply
// when using a custom persistence mechanism and how best to leverage those features.
//
// @title Custom Server DataSources
// @treeLocation Concepts/Persistence Technologies
// @visibility external
//<

//> @groupDef featuresCustomPersistence
// The vast majority of the SmartClient Server framework's key features are not specific to the
// built-in SQL and Hibernate connectors, and still apply even when using a custom persistence
// mechanism.
// <P>
// See the listing below of major features and how to apply them with custom persistence:
// <p>
// <b>Server Data Binding:</b> Using the SmartClient Server framework means that the starting
// point for connecting to custom persistence logic is a clean Java API.  SmartClient provides
// Java <code>DSRequest</code> and <code>DSResponse</code> objects with all of the methods
// necessary to handle data paging, sorting, validation error reporting, and other features.
// In most cases, you can fulfill a DSResponse by simply returning one of your Java
// business objects rather than worrying about how to encode objects to XML or JSON.
// Communication with the browser is automatically handled with an efficient, compressed
// protocol.
// +explorerExample{ormDataSource,Custom DataSource example},
// +explorerExample{DMI,DMI example}
// <p>
// <b>Data Selection (No DTOs):</b> When using a DataSource, Java data you return in your
// <code>DSResponse</code> is automatically trimmed to just the fields declared in the
// DataSource before delivery to the browser (see
// +link{DataSource.dropExtraFields,dropExtraFields}).  This eliminates the need to create
// redundant +externalLink{http://en.wikipedia.org/wiki/Data_transfer_object,Data Transfer Objects}
// to express the list of fields that need to be delivered to the UI - the DataSource already
// has this information, and can serve two purposes by both configuring UI components and
// trimming relevant data, from a single definition.
// <P>
// Furthermore, DataSources can extract specific fields from complex nested
// object graphs via XPath expressions, for both loading and saving of data.
// +explorerExample{flattenedBeans,XPath Binding example}
// <p>
// <b>Server Validation:</b> Both client and server validation are driven from declarations in
// a single DataSource definition.  You already have a need to declare validators to
// drive SmartClient's client-side validation; when you use the SmartClient Server framework
// you get automatic server-side enforcement of the same validation rules, without the need to
// write any additional code.
// +explorerExample{serverValidation,Server Validation example}
// <p>
// <b>Queuing:</b> Queuing allows multiple data load or save requests from different UI
// components to be transparently combined into a single HTTP request with guaranteed in-order
// execution.  The type of DataSource handling each request is not important, so queuing will
// work with your custom DataSource; in fact, a single queue could contain operations to be
// handled by many different types of DataSource.
// +explorerExample{transactionsFolder,Queuing examples}
// <p>
// <b>Declarative security:</b> The server framework provides robust authentication and
// authorization integration, to allow you to secure both DataSource operations (data fetch
// and update requests) and individual DataSource fields at various granularities.  The
// declarative security rules are expressed directly in the <code>.ds.xml</code> file, and
// allow you declare rules as simple as "all operations on this DataSource require an
// authenticated user", or as complex as "a user can only update this field if he has role
// 'admin' and a call to the <code>isAuthorized()</code> method on a security checking object
// we have stored in the <code>HttpSession</code> returns true".  Security rules are applied
// by the framework before and after a DSRequest is executed, so they are applied even to
// operations on completely custom DataSources. See
// +link{OperationBinding.requiresRole} and the properties it links to for more details on
// operation-level security, and +link{DataSourceField.viewRequiresAuthentication} and the
// properties it links to for more details on field-level security.
//
// <p>
// <b>Transaction Chaining:</b> allows one request in a queue of operations to incorporate
// values from previously executed requests in the same queue.  This allows a series of
// dependent operations - such as fetching a value from one DataSource to be used in a query on
// another - to be defined with simple declarations right in the DataSource definition, with no
// need to write a custom Java class to manually pass data between the different operations.
// Since Transaction Chaining works strictly in terms of DataSource requests and responses and
// knows nothing about the underlying persistence mechanism, it works with any persistence
// strategy.
// <p>
// <b>Java / JS Reflection:</b> Any Java object can be delivered to the browser as a JavaScript
// object, and vice versa.  As a developer of a custom DataSource, you do not need to concern
// yourself with dealing with translations to and from JSON or XML; you work directly with
// native Java objects.  For example, a method you write to fulfill a "fetch" operation can
// simply return a <code>Collection</code> of Java beans; the SmartClient Server framework would
// transparently handle converting this into a matching Javascript structure.
// +explorerExample{masterDetail,Saving nested objects example}
// <p>
// <b>Visual Builder:</b> The DataSource Wizards in Visual Builder are pluggable; we provide
// wizards for SQL and Hibernate DataSources, and it is easy to write a new wizard to integrate
// your custom DataSource into Visual Builder.  +explorerExample{sqlWizard,SQL Wizard screenshots},
// +explorerExample{hibernateWizard,Hibernate Wizard screenshots}
// <p>
// <b>Batch DataSource Generator:</b> If the persistence scheme you are implementing your
// custom DataSource for is based on collections of Javabeans, the Batch DataSource Generator
// can generate DataSource definition files for instances of your custom DataSource.  This is
// out of the box behavior, but you can also alter and extend the DataSource Generator to suit
// your exact needs - we supply the source and it has been specifically designed to be easy
// to modify.
// <p>
// <b>Batch Uploader:</b> A user interface for end-to-end batch upload of data as a pre-built,
// customizable component.  This component - like any SmartClient databound component - only
// cares that a DataSource is a DataSource, so custom DataSources will work just like built-in
// ones.  +explorerExample{batchUpload,Batch Uploader example}
// <p>
// <b>File Upload:</b> Single and multiple file uploads can be handled as a normal DataSource
// operation, including normal handling of validation errors. Optional automatic storage to SQL
// (no server code required) means you can upload to SQL tables for holding files, which can be
// related to Java Objects by id (eg, a User's uploaded files).
// +explorerExample{upload,File Upload example}
// <p>
// <b>Export:</b> Allows any grid component to export its current dataset in CSV, XML or JSON
// format.  This feature works by issuing the DataSource with an ordinary "fetch", and then
// changing the <code>DSResponse</code> to send back an import file rather than a resultset.
// Accordingly, this just works with custom DataSources.  +explorerExample{export,Export example}
// <p>
// <b>HTTP Proxy:</b> The HTTP Proxy allows an application to access web services hosted on
// remote servers which are not normally accessible to web applications due to the
// +externalLink{http://www.google.com/search?q=same+origin+policy,"same origin policy"}).
// This is a general feature of the SmartClient Server framework that does not directly apply
// to DataSources.  +explorerExample{rssFeed,HTTP Proxy example}
// <p>
// <b>Lightweight Persistence / Reporting:</b> Even while using a custom DataSource to connect
// to a custom ORM system, you can still make use of the SQL DataSource for simple storage-only
// entities where an object-based representation is a waste of time.  You can also do
// this for reporting scenarios that don't correspond to the object model.
//
// @title Server Features and Custom Persistence
// @treeLocation Concepts/Persistence Technologies
// @visibility external
//<


//> @groupDef sqlVsJPA
// If you are free to choose which persistence mechanism your application will use, you should
// consider using the SmartClient SQL DataSource instead of a more heavyweight, bean-based
// solution.  This article discusses the advantages of doing so.
// <p>
// <b>Simplicity</b>
// <p>
// With the SmartClient SQL DataSource, simple CRUD connectivity can be set up via a
// +explorerExample{sqlWizard,wizard} and requires zero server side code.  Only a DataSource
// descriptor (.ds.xml file) needs to exist; this descriptor can be generated by the wizard
// or created by hand.  The descriptor actually serves double duty by also providing the
// configuration for UI components - in other words, this is information that you would need
// to express anyway.
// <p>
// Semi-technical product managers, testers, business analysts and IT staff who have no
// familiarity with Java can easily comprehend DataSource definitions and even customized
// SQL queries, allowing them to go further with prototyping efforts, provide more specific
// feedback and capture more relevant diagnostics when reporting issues.
// <p>
// This level of simplicity is lost when using more heavyweight systems.  JPA / EJB
// best practices indicate creation of a bean class for every domain object, as well as
// related "services" or "session beans", DTOs
// (+externalLink{http://en.wikipedia.org/wiki/Data_Transfer_Object,Data Transfer Objects}) and
// other unnecessary scaffolding.  Ibatis avoids some of this scaffolding, but requires every
// SQL query to be written by hand.  In contrast the SQL DataSource supports basic CRUD queries
// out of the box.
// <p>
// <b>Performance</b>
// <p>
// Systems like JPA work nicely when dealing with a single object at a time, but enterprise
// applications routinely work with lists or trees of objects that draw data from multiple
// tables.  In these situations, it's trivial to express an efficient SQL query for retrieving
// the desired results (as shown in +explorerExample{largeValueMap,this example}).  Fetching the
// same data using getter methods on Java Beans often leads to nightmare performance scenarios
// (such as 3 or more separate SQL queries per object retrieved).
// <P>
// Trying to "trick" the persistence system into generating efficient queries doesn't make
// sense - this just leads to a far more complex and fragile solution that now requires deep
// knowledge of how the ORM system generates SQL as well as SQL itself.
// <P>
// SQLDataSource allows you to directly write SQL when it makes sense, and
// +link{attr:DataSource.beanClassName,to use beans} when object oriented approaches are
// clearer and simpler.  When you do write SQL directly, you override just the parts of the
// query that you need to change - you still leverage SQLDataSource's ability to generate
// cross-database SQL for complex search criteria, efficient data paging and sorting, even in a
// complex reporting query (see +explorerExample{dynamicReporting,this example}).
// <p>
// <b>Portability</b>
// <p>
// SmartClient DataSources provide cross-database portability like JPA and other solutions.
// However, DataSources can also be replaced with an entirely different integration strategy or
// entirely different server platform, such as a SOA architecture where the browser contacts
// WSDL web services directly.  The clear data requirements definition represented by a
// DataSource makes such drastic technology changes much easier with the SQL DataSource than
// with any other technology.
// <p>
// <b>Power</b>
// <p>
// The SQL DataSource has out of the box support for server-side advanced filtering without
// the need to write any code (see the
// +explorerExample{filterBuilderBracket,SQL Advanced Filtering example}), and SmartClient
// provides +link{class:FilterBuilder,pre-built user interfaces for filtering}.  The effort
// required to develop similar functionality with another persistence mechanism would vary from
// substantial to spectacular.
// <p>
// You can leverage advanced, automatic SQL generation, such as advanced filter criteria,
// GROUP BY and ORDER BY clauses, and selection of row ranges, even in very heavily customized
// queries.  The +explorerExample{dynamicReporting,Dynamic Reporting example} shows this.
// <p>
// With the SQL DataSource and Transaction Chaining, you can chain together multiple SQL
// queries, or a mixture of SQL queries and other data access, with simple declarations right
// in the DataSource, as +explorerExample{queuedAdd,this example} demonstrates.
// <p>
// Because you write the SQL, you can use database-specific features when absolutely
// necessary.  Features such as query optimizer hints or stored procedures are thus accessible
// but, importantly, are within the same processing model used for all other data access.
// <p>
// <b>Security</b>
// <p>
// Because the central DataSource definition expresses all the available operations, how they
// are performed and who has access to them, things are clear and simple.  It's much easier to
// understand and audit a DataSource definition than a slew of Java classes.
// <p>
// There is no information leakage from server to client with the SQL DataSource.  All
// server-side declarations, such as SQL templates, are automatically stripped out of the
// DataSource definition before the browser sees it.
// <p>
// Custom SQL in a SmartClient SQL DataSource is protected from SQL injection attacks.  It is
// impossible for a developer to write a SQL template that is vulnerable to SQL injection
// without going through the +link{group:velocitySupport,$rawValue} feature, a rarely used
// feature that is very prominently flagged in the documentation as requiring special care.
// Other ORM systems tend to require hand-coded SQL queries for advanced use cases such as
// reporting; these hand-written queries are where most security holes appear.  By providing a
// safe environment for SQL customizations, SQL DataSource removes these risks.
//
// @title SQL DataSource vs JPA, EJB, Ibatis and other technologies
// @treeLocation Concepts/Persistence Technologies
// @visibility external
//<


//> @groupDef serverDataIntegration
// Server Data Integration means:
// <ul>
// <li> You +link{iscInstall,install} the
//      +link{group:iscServer,SmartClient Java Server Framework} into any J2SE/J2EE
//      environment, including any existing web application
// <li> You +link{group:dataSourceDeclaration,create DataSources} via an XML declaration,
// possibly on-the-fly from +link{group:metadataImport,existing metadata}.
// <li> Server communication for components bound to these DataSources is handled
// automatically with a highly efficient, compressed protocol.  You work with clean Java APIs
// instead of dealing with the details of XML or JSON over HTTP.
// <li> You can use built-in connectors for SQL, Hibernate and other common data providers
// without writing any code, or you can easily build your own connectors in Java.
// <li> Whether using the built-in connectors or custom connectors, declarations in your
// DataSource control a large set of server features that can make common types of business
// logic entirely declarative
// </ul>
// This approach is in contrast to
// +link{group:clientDataIntegration,Client-side Data Integration} in which client-side
// DataSources are configured to send and receive HTTP messages containing XML, JSON
// or other content.
// <P>
// <B>Server-side Request Processing</B>
// <P>
// Client-side +link{DataBoundComponent,DataBoundComponents} will send
// +link{DSRequest,DSRequests} to the SmartClient Server as background communications transparent
// to the user.  Integrating SmartClient's DataSource layer with your data model is a matter of
// handling these DSRequests and sending back DSResponses, in order to fulfill the 4 basic
// operations of the +link{group:dataSourceOperations,DataSource Protocol}.
// <P>
// Out of the box, SmartClient is set up to route all DSRequests through a special servlet
// called <code>IDACall</code>.  Requests that go through <code>IDACall</code> have the
// following lifecycle:
// <ul>
// <li>The overall HTTP request is received by the IDACall servlet.  SmartClient supports
// queuing of transactions, so each HTTP request might contain multiple DSRequests.</li>
// <li>IDACall sets up an instance of <code>RPCManager</code> to manage the processing of
// the entire queue of transactions.  For every DSRequest in the queue, this RPCManager:</li>
//   <ul>
//   <li>Validates the DSRequest</li>
//   <li>Sends the DSRequest through +link{dmiOverview,DMI} - in other words, your code - if this is
//       configured in the DataSource.  As described later in this section, your code can
//       perform some custom logic here: either completely fulfilling the request, or
//       alternatively modifying the request and causing the default
//       processing of the request to continue</li>
//   <li>Calls the DataSource's <code>execute</code> method to obtain a DSResponse.</li>
//   </ul>
// <li>Having processed all requests, the RPCManager now serializes all the DSResponses
// and sends them back to the browser as a single HTTP response</li>
// </ul>
// <p>
// This basic request handling flow can be customized at a number of points:
// <ul>
// <li>If you need an overarching authentication service, this is best implemented using
// <a href=http://java.sun.com/products/servlet/Filters.html>servlet Filters</a> to intercept
// unauthenticated requests before they reach the <code>IDACall</code> servlet</li>
// <li>If you are not using one of the built-in persistence mechanisms (SQL and Hibernate),
// hook into the <code>IDACall</code> flow by
// +link{writeCustomDataSource,writing a custom DataSource}.  This approach lets you write and
// maintain the minimal amount of custom code, while taking full advantage of
// DataSource-agnostic features of the SmartClient Server, like validation, queuing,
// transaction chaining, support for Velocity templating, and so on.</li>
// <li>Custom validation can be added by writing a custom DataSource (extending SQLDataSource
// or HibernateDataSource if appropriate) and overriding its <code>validate()</code> method,
// as described +link{DataSource.serverConstructor,here}.</li>
// <li>General custom business logic can be added in a number of ways, both declaratively and
// programmatically:</li>
// <ul>
//   <li>The &lt;criteria&gt; and &lt;values&gt; properties of an +link{class:OperationBinding}
//       allow you to dynamically set data values at transaction-processing time, using
//       built-in +link{group:velocitySupport,Velocity support}</li>
//   <li>Override the <code>execute()</code> method of the DataSource to add extra processing
//       before and/or after the SmartClient processing</li>
//   <li>Use +link{attr:DSRequestModifier.value,Transaction Chaining} to dynamically set data values
//       according to the results of earlier transactions
//   <li>For SQL DataSources, use +link{group:customQuerying,SQL Templating} to change, add
//       to or even completely replace the SQL sent to the database, including calling
//       stored procedures</li>
//   <li>Use +link{dmiOverview,Direct Method Invocation} to call directly into your own Java
//       classes.  DMIs allow you to modify the <code>DSRequest</code> before it executes,
//       modify the <code>DSResponse</code> before it returns, or take unrelated actions.  They
//       can be used to add business logic to a persistence operation without destroying
//       the default behavior</li>
//   <li>Use +link{operationBinding.script,DMI Scripts} to add small amounts of business logic
//       right in the &lt;operationBinding&gt; tag.  DMI scripts allow you to add business
//       logic just like normal DMIs, but don't require the logic to be in a separate .java
//       file.</li>
// </ul><br>
// <li>If you need to use a Front Controller servlet for some other reason than authentication -
// for example, you are using Spring, Struts, or some other similar system which requires that
// all requests go through some particular servlet - just call
// <code>RPCManager.processRequest()</code> within your Spring Controller, Struts Action, or
// whatever the equivalent is in the framework in use.
// <p>
// However, note carefully that taking this approach is often a sign that the SmartClient
// architecture has not been correctly understood.  SmartClient is architected for
// <em>client-server</em> data communication, as opposed to early web MVC frameworks which
// do everything on the server.  In particular, it is absolutely incorrect to represent every
// individual DataSource operation - or even every DataSource - as a separate Struts Action
// or Spring Controller, because this implies different URLs for different operations.  All
// DataSource operations should go through a single URL in order to allow
// +link{class:RPCManager,transaction queuing} - see these
// +explorerExample{transactionsFolder,Queuing examples}.</li>
// </ul>
// <P>
// For more information on the DMI subsystem, see the +link{dmiOverview,DMI overview},
// +link{DMI,DMI class} and the
// +explorerExample{DMI,DMI example} in the Feature Explorer.
// <P>
// Note that, as you continue to integrate your prototype with your backend, you can use a
// mixture of DataSources that have been fully integrated with your backend and DataSources
// that are running in "client-only" mode (see +link{group:clientOnlyDataSources}).
// <P>
// <b>Important methods for handling DataSource requests</b>
// <P>
// The basic flow of logic for handling DataSource requests is:
// <P>
// <table class="normal" border=1 width="700">
// <tr>
// <td>1. Determine operation type (Fetch, Add, Update, Remove) for a single request.  Not
// necessary if you follow the recommendations for
// +link{group:writeCustomDataSource,writing a custom DataSource} and provide your
// implementation via <code>executeFetch(), executeAdd()</code>, et al.</td>
// <td>dsRequest.getOperationType()</td>
// </tr>
//
// <tr>
// <td>2. Get inbound values (Add, Update) and/or criteria (Fetch, Update, Remove) for this
// request.</td>
// <td>dsRequest.getFieldValue()<br>
// dsRequest.getValues()<br>
// dsRequest.getCriteria()</td>
// </tr>
//
// <tr>
// <td>3. Business logic, validation, calls to data and service tiers... anything you can code.
// </td>
// <td><b>execute custom logic</b></td>
// </tr>
//
// <tr>
// <td>4. Set status and data for the response.</td>
// <td>dsResponse.setStatus()<br>
// dsResponse.setData()</td>
// </tr>
// </table>
// <P>
// For more information, see the +link{RPCManager,RPCManager documentation}, and the
// +explorerExample{ormDataSource,Custom ORM DataSource example}.
//
// @title Server DataSource Integration
// @treeLocation Concepts/Client-Server Integration
// @treeLocation Java Server Reference
// @visibility external
//<

//> @groupDef metadataImport
// In SmartClient, metadata is expressed through +link{DataSource,DataSources}, which in turn
// drive +link{DataBoundComponent,DataBoundComponents}.  If you have existing metadata, there
// are several possible approaches to transforming it to SmartClient DataSources, either one
// time or on the fly.
// <P>
// There are two possible targets for metadata import: XML format or JavaScript format.
// The XML format is more general purpose, since the ISC server can transform it to JavaScript
// via the +link{group:loadDSTag,loadDS tag}, and DataSources in XML format can be used by the
// ISC server for server-side validation (this split is covered in more detail under
// +link{dataSourceDeclaration,Data Source Declaration}).
// <P>
// You may also transform your metadata dynamically (while the application is running in
// production) or statically (one time ever or at packaging time).  Generally for a static
// or dynamic transform targetting JavaScript format you will want to produce one .js file
// containing all your DataSource definitions, to be loaded by your application via a normal
// &lt;SCRIPT SRC&gt; tag.  For a static transform targetting XML format, you will want to
// produce a series of .ds.xml files and place them in the directories expected by the ISC
// server (see +link{group:dataSourceDeclaration,DataSource Declaration}).  Statically generated
// XML DataSources can be delivered to the browser as a single .js file via a .jsp containing
// several +link{loadDSTag,<code>loadDS</code> tags}.
// <P>
// If you want to do dynamic transform targetting XML format and use ISC server-side
// validation, the server-side API DataSource.fromXML() can be used to create a DataSource
// dynamically from XML, so that you can then call DataSource.validate().  Either the XML
// DataSource definition or the live DataSource itself can be passed to the server-side API
// XML.toJS() to produce JavaScript.
// <P>
// How to actually produce JavaScript or XML DataSource definitions from your existing metadata
// depends on the format of your metadata.
// <P>
// <b>XML Schema</b>
// <P>
// The method +link{XMLTools.loadXMLSchema()} and the
// +link{group:loadXMLSchemaTag,loadXMLSchema JSP tag} provide dynamic transform of XML Schema
// to JavaScript.  This is essentially accomplished by running
// <code>isomorphic/system/schema/schemaTranslator.xsl</code> on the XML schema file to produce
// XML DataSource definitions, and then translating those to JavaScript.  You can run the
// <code>schemaTranslator</code> stylesheet using any standard XSLT processor and capture the
// XML output.
// <P>
// <b>Java Beans</b>
// <P>
// Metadata available via Java's "reflection" APIs allows a basic DataSource to be generated
// from Java beans.  Sample Java code can be found in
// <code>examples/server_integration/DataSourceGenerator.java</code>.  See also the last section
// on this page for an automatic option for generating DataSource definitions via Reflection.
// <P>
// <b>Other XML formats</b>
// <P>
// If you are familiar with XSLT or other XML transform languages, you
// could use it to do an XML to XML transform, and then use XML.toJS() to get to JavaScript.
// <P>
// <b>Schema represented as Java Objects</b>
// <P>
// If you are targetting XML, hand-coded generation of DataSource XML is straightforward, and
// from XML you can use XML.toJS() to get to JavaScript.
// <P>
// <b>Database Schema, Hibernate mappings or Java class definitions</b>
// <P>
// <em><b>Note: This is an Enterprise feature.</b>  It is not available to users of the LGPL,
// Pro or Power Editions of SmartClient.  Please see the <a href=http://smartclient.com/licensing>
// licensing page</a> for details.</em>
// <p>
// If you have existing database tables, Hibernate classes or Java POJOs that follow the Javabean
// semantics, you can use these to automatically produce basic XML DataSource definitions.
// We provide the Batch DataSource Generator, a supported tool that makes use of SmartClient
// Server APIs to generate XML DataSource definitions and save them as <code>.ds.xml</code> files
// in your <code>shared/ds</code> folder.  The source for the tool is provided in
// <code>tools/batchDSGenerator.jsp</code>.
// <p>
// If your metadata source is existing database tables, the Batch DS Generator can also extract
// data from those tables and save it as test data in your <code>shared/ds/test_data</code>
// folder.  With this option, you have a complete round-trip facility from a populated database
// table to a client-side DataSource that does not require a server. The database
// table can be recreated and repopulated from the XML files using the SmartClient Admin
// Console, giving you a portable schema and dataset that can be used to initialize any database.
// <p>
// The tool is also able to output the DataSource definitions and test data in Javascript format
// rather than XML, for direct inclusion in client-side programs.  In this mode, it will create
// <code>.ds.js</code> and <code>.data.js</code> files, rather than files with a <code>.xml</code>
// extension.
// <p>
// To use the tool, direct your web browser to the <code>tools/batchDSGenerator.jsp</code>
// resource of your SmartClient server.  If you pass no parameters to the JSP, a SmartClient
// window will be shown, prompting you for the various generation parameters.  Alternatively,
// you can effect a true batch operation by passing these parameters in directly.  The source
// code is extensively commented and explains the meanings of all the parameters should you wish
// to take this latter approach.
// <p>
// Example simple usage (via the UI):<br/>
// <code>http://localhost:8080/tools/batchDSGenerator.jsp</code>
// <p>
// Example batch usage (providing the parameters by hand):<br/>
// <code>http://localhost:8080/tools/batchDSGenerator.jsp?dbName=Mysql&tableName=foo&tableName=bar&className=com.bar.foo.SomeClass&overwrite=true</code>
//
// @title Metadata Import
// @treeLocation Client Reference/Data Binding
// @visibility external
//<


//> @groupDef errorHandling
//
// +link{RPCResponse} and +link{DSResponse} objects have an integer status field that the
// RPCManager inspects when the response is received from the server. If the value of this
// field is less than zero, the request is considered to have failed.  Otherwise it is
// considered to have succeeded.  This value is settable via the setStatus() method call
// on the server-side DSResponse and RPCResponse objects.
// <p>
// Errors in a SmartClient application fall into two main categories:<ul>
// <li>Validation errors, which arise as a result of rules in the application's business logic
// being broken.  These are part of the normal operation of the system.  A response with
// validation errors has a status of +link{RPCResponse.STATUS_VALIDATION_ERROR}</li>
// <li>Unrecoverable errors, which are errors with the system itself.  These are not part of
// the normal operation of the system</li>
// </ul>
// <b>Validation errors</b> are treated differently from other errors, precisely because they
// are an expected part of the normal operation of the system.  If
// +link{DynamicForm.validate,validation} of a form results in errors, the form is redrawn to
// display those errors to the user.  How the user sees those errors is completely configurable -
// for example, see the DynamicForm properties +link{DynamicForm.showErrorIcons,showErrorIcons},
// +link{DynamicForm.showErrorText,showErrorText},
// +link{DynamicForm.showInlineErrors,showInlineErrors}, and indeed most DynamicForm properties
// that contain the workd "Error" - but the default in most skins is to highlight the field
// with some kind of error icon, and provide the actual error text message in a floating box
// when the user hovers over the field.
// <p>
// The remainder of this article concerns <b>unrecoverable errors</b>.  These are errors with
// the system itself, for example:<ul>
// <li>A network transport problem</li>
// <li>A server-side crash</li>
// <li>An update failed because a transaction was rolled back</li>
// </ul>
// Errors like this can either be handled centrally, or you can choose to handle them in your
// regular callback code.  +link{class:DSRequest} calls default to centralized handling;
// +link{class:RPCRequest} calls default to user error handling in the callback.
// <p>
// <b>Centralized Error Handling</b><br>
// If the status field shows a failure, the RPCManager will invoke
// +link{RPCManager.handleError}.  By default, this logs a warning and shows a dialog
// with the contents of the response's +link{rpcResponse.data,data} field (which is assumed
// to contain a meaningful description of the error that occurred).  If you specified a
// callback in your request, it will <b>not</b> be called if the status shows a failure
// (see the section on custom error handling below for how to change this).  This default
// arrangement means that any SmartClient application has a basic handling mechanism for
// unrecoverable errors, without any code to write.
// <p>
// You can customize centralized error handling at two levels:<ul>
// <li>
// <smartclient>Override +link{RPCManager.handleError}</smartclient>
// <smartgwt>Use
// {@link com.smartgwt.client.rpc.RPCManager#setHandleErrorCallback(com.smartgwt.client.rpc.HandleErrorCallback)}</smartgwt>
// to provide your own error handling logic (note
// that customization takes place at the static class level, not per-instance)</li>
// <li>
// <smartclient>Override +link{RPCManager.handleTransportError}.</smartclient>
// <smartgwt>Use
// {@link com.smartgwt.client.rpc.RPCManager#setHandleTransportErrorCallback(com.smartgwt.client.rpc.HandleTransportErrorCallback)}.</smartgwt>
// This logic is called earlier than
// handleError, and it is called even when you are using custom error handling (discussed
// below).  It is intended to allow your code to inspect the failed response early in the
// handling flow, to see if it is really unrecoverable.  For example, a failure might have
// happened because of a temporary network problem, so resubmitting the request may be a valid
// thing to do to try to resolve the error.  Note, as with handleError, this is a static
// class-level customization</li>
// </ul>
// <p>
// <b>Custom Error Handling</b><br>
// As an alternative to handling errors centrally, you can handle them in your regular callback
// methods.  To do this, specify +link{RPCRequest.willHandleError,willHandleError} as a
// request property.  When you do this, centralized error handling is bypassed (as mentioned
// above, <code>handleTransportError()</code> will still be called) and your callback is
// invoked as normal.  Your callback code determines that it is in error state by inspecting
// the status property on the response - if it is negative, there has been an error.  Note
// that validation errors are treated specially, in that your callback is invoked, but the
// normal behavior of populating the field errors onto the form and redrawing it <b>also</b>
// takes place.
// <p>
// Note, when you are handling errors in user callbacks, a negative status in the response
// indicates some sort of serious, unrecoverable error (except in the case of
// +link{RPCResponse.STATUS_VALIDATION_ERROR}).  Therefore, ensure that your error handling
// code does not assume that the response will be properly formed or contain particular
// elements.
// <p>
// You can specify <code>willHandleError</code> (or any other DSRequest/RPCRequest property)
// on a component request by providing the DSRequest Properties parameter.  For example, on
// a +link{ListGrid.fetchData()}:
// <smartclient><pre>
//     listGrid.fetchData({}, function(dsResponse, data, dsRequest) {
//         if (dsResponse.status < 0) {
//             // Error handling here...
//         } else {
//             // Normal processing here
//         }
//     }, <b>{willHandleError: true}</b>);
// </pre>
// </smartclient>
// <smartgwt><pre>
//     DSRequest properties = new DSRequest();
//     properties.setWillHandleError(true);
//     listGrid.fetchData(new Criteria(), new DSCallback() {
//         public void execute(DSResponse response, Object rawData, DSRequest request) {
//             if (response.getStatus() < 0) {
//                 // Error handling here
//             } else {
//                 // Normal processing here
//             }
//         }
//     }, <b>properties</b>);
// </pre>
// </smartgwt>
// <b>Error Status Codes</b><br>
// The error status codes used by the framework are documented as class variables of the
// +link{class:RPCRequest,RPCRequest class}.  Status codes in the range -1 to -100 are
// reserved for use by the framework; if you wish to introduce new custom error statuses for
// your own use, avoid this range.
// <p>
// <b>Errors indicating login is required</b><br>
// Some of the framework error statuses indicate that login is required, typically because a
// user's HTTP session has timed out.  The best way to handle this situation is to use the
// built-in +link{group:relogin,re-login flow} to automatically prompt users to log back
// in as required, and then resubmit the requests that triggered the login required response.
//
// @title Error Handling Overview
// @treeLocation Client Reference/Data Binding
// @visibility external
//<

    // Comm and Transactions
    // ----------------------------------------------------------------------------------------

    // ---------------------------------------------------------------------------------------
    //> @object OperationContext
    // OperationContext is the bundle of settings that can be passed to performOperation() as
    // the "context" argument, or to any method that ultimately performs an operation and
    // supports a context argument.
    // <P>
    // There is no need to instatiate an OperationContext instance.  Just pass a normal
    // JavaScript object with the desired properties.
    //
    // @treeLocation Client Reference/Data Binding/RPCManager
    // @visibility ida
    //<

    //> @attr operationContext.showPrompt (boolean : true : IR)
    //  Whether to show a prompt in a modal dialog while this operation is being performed, in
    //  order to prevent user interaction.
    // @visibility ida
    //<

    //> @attr operationContext.prompt (string: "Contacting server..." : IR)
    //  Text to show in the operation prompt if one is shown.
    // @see showPrompt
    // @visibility ida
    //<

    //> @attr operationContext.afterFlowCallback (callback : null : IR)
    //  An additional callback that will be invoked after the callback passed as an argument to
    //  performOperation.
    // @visibility ida
    //<

    //> @attr operationContext.willHandleError (boolean : false : IR)
    // Set this flag to indicate you are willing to handle error results from the server.  If
    // this flag is not set, standard RPCManager/DataSource error handling will be invoked.
    // <p>
    // The most common use of this flag is to directly handle validation errors.
    // <p>
    // Note that if you see context.willHandleError, the OperationResult object passed to your
    // IDACallback may be missing properties or be otherwise malformed, depending on
    // the severity of the server error.
    // <p>
    //
    // @see class:OperationResult
    // @see RPCManager.handleError()
    // @see dataSource.handleError()
    // @visibility ida
    //<

    //> @attr operationContext.operation (OperationID : null : IR)
    // Only applicable when calling Action Methods, specifies the operation ID to use instead
    // of the default DataSource operation.
    //
    // @group dataBoundComponentMethods
    // @see group:dataBoundComponentMethods
    //<


    // ---------------------------------------------------------------------------------------
    //> @type IDACallback
    // A function to call or expression to evaluate when an operation completes.
    // <p>
    // An IDACallback can be:
    // <ul>
    // <li>a string expression
    // <li>an object with the properties "caller" and "methodName", indicating an object to be
    // called and the method to call on it
    // </ul>
    // Regardless of the form of the callback, the callback method will be invoked with the
    // arguments (operationResult, results, data, context) with the meaning:
    // <ul>
    // <li>operationResult: an OperationResult object containing all the data returned by the
    // server for this operation
    // <li>data: just the "result.data" property from the OperationResult, for convenience
    // <li>context: just the "context" property from the OperationResult, for convenience
    // </ul>
    // @see class:OperationResult
    // @treeLocation Client Reference/Data Binding/RPCManager
    // @visibility ida
    //<
    // NOTE: intentionally omitted mention of passing an IDACallback that is the name of a
    // function on the IDA instance.


//> @groupDef smartArchitecture
// SmartClient can add interactivity and performance benefits to any web application with a
// variety of integration approaches.  This topic discusses the optimal architecture for a
// SmartClient application, which can be adopted in whole or in part.
// <p>
// In a typical HTML-based web application, every time a new view is shown to a user, a round
// trip to the server is required to retrieve new presentation information, such as a search
// screen.  However in an ISC-based application, showing a new view can be accomplished by
// simply hiding some components and showing others.
// <p>
// Because ISC components are expressed in a concise declarative form, and because ISC
// components have essentially no runtime performance impact until used, dozens of application
// views can be downloaded to the browser using the same bandwidth that would have been
// required to render just the initial view in plain HTML.
// <p>
// This architectural pattern of "preloading views" has tremendous benefits.  View transitions
// which do not require new data from the server can be performed near-instantaneously and
// without server involvement, boosting both interactivity and scalability.
// <p>
// Showing a dialog containing a "wizard" is a straightforward example of showing a "preloaded
// view".  For example:
// <pre>
//    function showNewUserWizard() {
//        Window.create({
//            items:[
//                DynamicForm.create({ ... })
//            ]
//        });
//    }
//    Button.create({
//        title:"New User..",
//        click:"showNewUserWizard()"
//    });
// </pre>
// In this example, none of the components involved in a potentially multi-pane wizard are
// created until they are needed.  Showing the wizard has near-instantaneous response and
// causes no server load.
// <p>
// However, let's say that the first pane of the wizard is going to incorporate some dynamic
// user-specific data, such as the current user's name.  To load the username, we'll use an RPC
// operation targetting a .jsp called "getUserName.jsp" and show the wizard when it completes
// (see +link{class:RPCManager} for information on RPCs and how to construct a .jsp that can
// send an RPC response).
// <pre>
//    function showNewUserWizard() {
//        RPCManager.sendRequest({
//            actionURL:"getUserName.jsp",
//            callback:"doShow(rpcResponse)"
//        });
//    }
//    function doShow(rpcResponse) {
//        Window.create({
//            items:[
//                Canvas.create({contents:"Hello, " + rpcResponse.userName}),
//                DynamicForm.create({ ... })
//            ]
//        });
//    }
//    Button.create({
//        title:"New User..",
//        click:"showNewUserWizard()"
//    });
// </pre>
// In this example, we've simply incorporated a user name into the first pane of a wizard.
// However, this pattern allows us to arbitrarily change user interactions based on data from
// the server.  For example, the RPCResponse might have contained a flag indicating that the
// wizard should skip the first two steps, or an arbitrary warning message for the user, or
// even JavaScript code to be evaluated on the client.
// <P>
// This architecture has several key advantages:
// <dl>
//
// <dt><b>Performance: Cacheable UI</b></dt>
// <dd>
// A dynamic, data-driven UI can be expressed completely in <i>cacheable</i> JavaScript.
// This is in contrast to any architecture based on server-side HTML generation, where static
// parts of the presentation are mixed in with dynamic data, preventing cacheability so that
// bandwidth and server time are wasted repeatedly delivering the same static presentation
// data.
// <br>
// Even generated JavaScript is cacheable.  For example, a SmartClient View expressed in XML
// and contained within a JSP is still a separately cacheable resource when loaded via a
// &lt;SCRIPT SRC&gt; tag and advertised as a cacheable resource via HTTP headers, because it
// is ultimately delivered to the browser as simple JavaScript.  Hence standard
// +link{group:i18n,internationalization} techniques such as using JSTL tags in a JSP remain
// applicable.
// <br>
// The SmartClient Architecture even allows you to capture all the gradations of cacheability
// from completely static (changes once per application rollout) to completely dynamic
// (timestamp).  In the example above, the user name wouldn't actually change for the lifetime
// of the page, so could be loaded once only.
// <br>&nbsp;</dd>
//
// <dt><b>Performance: Minimal Server State</b></dt>
// <dd>
// Any architecture that relies on component descriptions being generated by the server
// must track a great deal of state, which, in the SmartClient Architecture, is either
// completely eliminated or greatly reduced.
// <br>&nbsp;</dd>
//
// <dt><b>True Presentation / Business Logic separation</b></dt>
// <dd>
// The RPCResponse object represents the client's exact, minimal needs for server data.
// This is much easier to understand and to audit than a slew of .jsp files which access and
// update miscellaneous state.  It is also far easier to spot reusable patterns of data access,
// which in server-side HTML generation systems often end up as duplicated code.
// <br>&nbsp;</dd>
//
// <dt><b>Parallel Development and Testability</b></dt>
// <dd>
// Using the SmartClient architecture allows you to build a complete, working application
// that can run without a server, based on sample data.  In the example above, it would be
// straightforward to create a testing mode that returned a faked RPC response consisting of
// simply <code>{ userName : "Bob" }</code>.
// <br>
// This allows better parallel development by enabling the client side of the system to be
// tested in isolation, and creates clearer communication between client and server-side
// developers since creation of test data tends to develop into data requirements
// specifications.
// <br>
// For more info on creating applications that support client-only testing, see
// +link{group:clientOnlyDataSources,Client Only DataSources}.
// <br>&nbsp;</dd>
// </dl>
// <br>
// <h3>Refinements</h3>
// <br>
// <b>Creating vs Showing a View</b>
// <br>
// Many views will be shown to the user repeatedly, for example, the user may repeatedly switch
// back and forth between two panes of a TabSet.  In that usage it makes sense to make a
// distinction between <i>creating</i> a view and <i>showing</i> an existing view.  When
// showing an existing view, the same components and/or data may be able to be reused.
// <br>
// In the following variant on the original example, we only create the Window object and
// do the RPC to retrieve the user name the first time <code>showNewUserWizard()</code> is
// called.  Subsequently we reuse the existing window, and we assume the user name has not
// changed, so we need not do the RPC again. (<i>Note: "New User" button omitted for brevity
// from here on</i>)
// <pre>
//    function showNewUserWizard() {
//        if (!window.myWindow) {
//            Window.create({
//                ID:"myWindow",
//                autoDraw:false,
//                items:[
//                    Canvas.create({ ID: "welcomeCanvas" }),
//                    DynamicForm.create({ ... })
//                ]
//            });
//            RPCManager.sendRequest({
//                actionURL:"getUserName.jsp",
//                callback:"doShow(rpcResponse)"
//            });
//        } else {
//            myWindow.show();
//        }
//    }
//    function doShow(rpcResponse) {
//        welcomeCanvas.setContents("Hello, " + rpcResponse.userName);
//        myWindow.show();
//    }
// </pre>
//
// <b>Batching Operations</b>
// <br>
// A view may incorporate multiple components, each of which requires data.  In the following
// example, a DataBound ListGrid has been incorporated into the wizard, and we'd like to fetch
// the user's name and the beginning dataset for the grid in the same batch.  We use
// +link{RPCManager.startQueue()} to do so.
// <pre>
//    function showNewUserWizard() {
//        if (!window.myWindow) {
//            Window.create({
//                ID:"myWindow",
//                autoDraw:false,
//                items:[
//                    Canvas.create({ ID: "welcomeCanvas" }),
//                    <b>ListGrid.create({
//                        ID: "myGrid",
//                        dataSource:"myDataSource"
//                    }),</b>
//                    DynamicForm.create({ ... })
//                ]
//            });
//            <b>RPCManager.startQueue();
//            myGrid.fetchData();</b>
//            RPCManager.sendRequest({
//                actionURL:"getUserName.jsp",
//                callback:"doShow(rpcResponse)"
//            });
//            <b>RPCManager.sendQueue();</b>
//        } else {
//            myWindow.show();
//        }
//    }
//    function doShow(rpcResponse) {
//        welcomeCanvas.setContents("Hello, " + rpcResponse.userName);
//        myWindow.show();
//    }
// </pre>
//
// <b>Segmenting very large Applications</b>
// <P>
// If an application has many hundreds of views, but only a handful of views are used by a
// given user in a typical session, for the fastest loading performance you should consider
// loading only the most commonly used views initially then loading further views on demand.
// <P>
// You can use +link{FileLoader.loadJSFiles()} to load a set of JavaScript files
// compromising an application module that defines a set of related views.  The loaded
// JavaScript files may define new component classes and new DataSources in addition to
// defining new views and their associated logic.
//
// @title SmartClient Architecture
// @treeLocation Concepts
// @visibility external
//<






//> @class DMI
// Static singleton class with APIs for +link{group:dmiOverview,Direct Method Invocation} of
// server side methods when running the SmartClient java server.
//
// @treeLocation Client Reference/RPC
// @visibility external
//<

//> @groupDef dmiOverview
//
// Direct Method Invocation (DMI) allows Ajax requests from the UI to directly
// invoke methods on server-side objects via XML configuration.  See also
// +link{operationBinding.script,DMI Scripts}, which allows you to place code directly into the
// XML file instead of in a separate .java file; this is a useful approach when DMI code is
// short.
// <P>
// When using DMI, request data from the UI is translated to Java objects and passed to the
// Java method you designate with an XML declaration.  Your Java method simply declares the
// parameters it needs and they are automatically provided (see "Method Invocation" below).
// The return value of your method is automatically wrapped as a valid response and delivered
// to the browser.
// <P>
// DMI requires the +link{iscServer,SmartClient Server}.  Note that SmartClient also supports
// several approaches for interacting with +link{group:nonJavaBackend,non-Java backends} and/or
// Java backends not running the ISC server.
// <P>
// <u><b>DataSource DMI</b></u>
// <br>
// See also +link{serverDataIntegration,Server DataSource Integration} overview.<br>
// To enable DMI for a given DataSource, simply include a <code>&lt;serverObject&gt;</code>
// configuration block in that DataSource's configuration either at
// +link{DataSource.serverObject} or on a particular operationBinding via
// +link{OperationBinding.serverObject}.  The ServerObject specifies the target of the method
// invocation and +link{OperationBinding.serverMethod} specifies the method that will be
// called.
// <P>
// For example, the following Datasource DMI declaration would route "fetch" operations for
// this DataSource to the method "fetch" on an object stored in the servlet session under the
// name "beanFetcher":
// <pre>
// &lt;DataSource&gt;
//   &lt;operationBindings&gt;
//       &lt;binding operationType="fetch" serverMethod="fetch"&gt;
//           &lt;serverObject
//                lookupStyle="attribute"
//                attributeScope="session"
//                attributeName="beanFetcher"/&gt;
//       &lt;/binding&gt;
//   &lt;/operationBindings&gt;
//   ...
// &lt;/DataSource&gt;
// </pre>
// Method overloading is not supported - there must be exactly one method on the target class
// with the name specified in +link{OperationBinding.serverMethod}.  The method must be public,
// but can be either an instance or static method.  If no operationBinding is specified or the
// operationBinding does not specify a <code>serverMethod</code> then it defaults to the name of
// the operation (eg "fetch").
// <p>
// By default, the DSResponse data sent back by DataSource DMIs is filtered to just the set of
// fields specified on the DataSource.  This allows you to simply return beans that potentially
// have getter methods for fields other than are defined in the DataSource without that
// (potentially private) data being sent to the client.  If you want to disable this
// functionality, you can do so on a per-operation basis by setting
// +link{ServerObject.dropExtraFields}, on a per-DataSource level by setting
// +link{DataSource.dropExtraFields}, or globally by setting the config parameter
// <code>DMI.dropExtraFields</code> to <code>false</code> in
// +link{server_properties,[webroot]/WEB-INF/classes/server.properties}.
// Non-DMI DSResponse data is, by
// default, not filtered in this manner for backward compatibility reasons.  If you want to
// enable this type of filtering for non-DMI DSResponse data, you can do so by setting the
// config parameter <code>DSResponse.dropExtraFields</code> to <code>true</code> in
// +link{server_properties,[webroot]/WEB-INF/classes/server.properties}.
// <code>DMI.dropExtraFields</code>
// and <code>DSResponse.dropExtraFields</code> can be enabled/disabled independently of each
// other - that is, setting one does not side-effect the other.  +link{server_properties,server.properties}
// settings can be overridden by an explicit setting in +link{dataSource.dropExtraFields} which
// in turn can be overridden by an explicit setting in +link{serverObject.dropExtraFields} (this
// last one for DMI only since non-DMI operations don't have a serverObject context).
// <p>
// <u><b>DataSource DMI and regular RPCManager dispatch</b></u><br>
// It is possible to use DMI to incorporate your own code into what is otherwise the regular
// process flow of an ordinary, non-DMI DataSource request.  This is particularly valuable if
// you are using the built-in SQL or Hibernate DataSources, because it allows you to inject
// extra functionality (validations, processing steps, messages to other systems, anything you
// can code) into a fetch or update request that is otherwise handled for you by the SmartClient
// Server.
// <p>
// To do this, just configure an operationBinding for DMI, as described above.  Then, in your
// server-side method, invoke <code>execute()</code> on the <code>DSRequest</code> your method
// is passed.  If you create a DMI method that does nothing <b>but</b> invoke
// <code>dsRequest.execute()</code>, then you have a DMI method that behaves exactly like the
// normal RPCManager dispatch code.  Customizing "normal RPCManager dispatch code" is now a
// simple matter of adding logic to your DMI method.  See
// +explorerExample{userSpecificData,this example} of this technique in the Feature Explorer.
// <p>
// <u><b>RPC DMI</b></u>
// <br>
// RPC DMI makes a set of methods from a server-side class available as client-side methods for
// direct invocation (via +link{DMI.call()}). This provides a way to perform arbitrary
// client/server interactions outside of the DataSource subsystem.
// <P>
// RPC DMI is an alternative approach to using the +link{RPCManager} class directly to send
// requests to some <code>actionURL</code> where your server code would receive a generalized
// +link{RPCRequest,request object}, to be routed to appropriate
// methods yourself.  Which interface (DMI or RPCManager) you choose is largely a matter of
// preference - they provide equivalent functionality.
// <P>
// RPC DMI also uses a +link{ServerObject} configuration block to specify
// the server-side DMI end-point, but in the case of RPCs, the +link{ServerObject} definition
// goes into an <code>rpcBindings</code> section of an
// +link{group:applicationDeclaration,Application definition} in a <code>*.app.xml</code> file.
// <smartclient>For an example, see the <code>example.app.xml</code> file
// in the /shared/app directory of the SmartClient SDK.</smartclient>
// The only difference between the RPC DMI
// ServerObject definition and the DataSource DMI version is the addition of the
// +link{ServerObject.visibleMethods} block that specifies which methods are callable on this
// ServerObject.  This section is not consulted for DataSource DMIs because the
// +link{OperationBinding.serverMethod} is used to specify the callable method in that case.
// <p>
// <u><b>Method Invocation</b></u>
// <br>
// SmartClient can pass a set of stock context variables to your DMI method and also performs
// some type adaptation logic to make this interface more flexible.  For DataSource DMI, you
// can declare your method to take any number of the following types of arguments and they will
// be passed to you:
// <ul>
// <li>HttpServletRequest
// <li>HttpServletResponse
// <li>ServletContext
// <li>HttpSession
// <li>RPCManager
// <li>DSRequest
// <li>RequestContext (from com.isomorphic.servlet)
// <li>DataSource (same as DSRequest.getDataSource())
// <li>Map (same as DSRequest.getValues())
// <li>Bean (auto-populated from DSRequest.getValues())
// </ul>
// DataSource DMI methods can return any of the following types of values:
// <ul>
// <li>DSResponse (used as the DSResponse verbatim)
// <li>List (valid response to a fetch operation - gets auto-popuplated into a DSResponse for
// you via setData())
// <li>Map or Bean (valid response to add, update, remove operations - gets auto-populated
// into a DSResponse for you via setData()).
// </ul>
// Note that to take advantage of some SmartClient features like paging and custom validation,
// you need to return a DSResponse and provide the required metadata (like
// startRow/endRow/totalRows for paging).  You can simply return a <code>List</code> instead,
// but this won't work for large datasets.
// <p>
// So, for example, all of the following DataSource DMI method signatures are valid:
// <pre>
// public List myFetch(Map criteria)
// public List myFetch(SupplyItem criteria)
// public DSResponse myAdd(HttpSession session,
//                         DataSource ds,
//                         DSRequest dsRequest)
// </pre>
// <p>
// See
// +externalLink{/examples/server_integration/#customDataSourceIntegrationDMI,the supplyItemDMI example}
// for an example of DataSource DMI.
// <p>
// <p>
// RPC DMIs work slightly differently.  Unlike DataSource DMIs, RPC DMIs can have an arbitrary
// number of required arguments, and also some optional context arguments.  For example, let's
// say you call a method from the client like so
// <smartclient>(note that there's a cleaner way to invoke
// DMIs if you use the +link{group:loadDMIStubsTag} JSP tag)</smartclient>:
// <br>
// <smartclient><pre>
// DMI.call("myApp", "com.sample.MyClass", "doSomething",
//          1, "zoo", [1, 2, 3], "clientCallback()");
// </pre>
// </smartclient>
// <smartgwt><pre>
//        List someList = new ArrayList();
//        someList.add(1);
//        someList.add(2);
//        DMI.call("myApp", "com.sample.MyClass", "doSomething", new RPCCallback() {
//
//            &#64;Override
//            public void execute(RPCResponse response, Object rawData, RPCRequest request) {
//
//                SC.say("raw data from server method:" + rawData.toString());
//
//
//            }
//        }, new Object[] {1, "zoo", someList});
// </pre>
// </smartgwt>
// The server-side implementation of the method invoked must have a signature that
// will accept the arguments passed in from the client. In the example above
// <code>com.sample.MyClass.doSomething</code> should accept a Number, String,
// and a List as arguments.  SmartClient will try to adapt arguments where possible -
// so for example the first argument can be a Long or an Integer, or a native type
// (<code>long</code> or <code>int</code>) instead and the
// invocation will still work.
// <smartclient>
// JavaScript objects passed from the client becomes a Map on the server and will
// be automatically applied to a bean if the method argument takes a Bean in that position.
// </smartclient>
// <smartgwt>
// If a Map is passed from the client to the server it will
// be automatically applied to a bean if the method argument takes a Bean in that position.
// </smartgwt>
// See +link{RPCRequest.data} for a table of type conversions.
// <P>
// In addition to the parameters explicitly passed from the client, your method signature
// can include some additional arguments to pick up information about the request passed in.
// If your server side method is declared with arguments of the following type they will
// be passed to your DMI method.
// <ul>
// <li>HttpServletRequest
// <li>HttpServletResponse
// <li>HttpSession
// <li>RPCManager
// <li>RPCRequest
// </ul>
// <P>
// Your server side method can return a <code>RPCResponse</code> object giving you full
// control over the response sent to the server. If your method does not return a response,
// one will be created automatically and the return value from the server method will become the
// <code>data</code> value on the response. <br>
// See +link{RPCRequest.data} for an overview of how server side java data types are mapped
// to client side values.
// <smartclient>
// <p>
// See
// +externalLink{/examples/server_integration/#genericRPCIntegrationDMI,the getTimeStampDMI example}
// for an example of RPC DMI.
// </smartclient>
//
// @see group:applicationDeclaration
// @see group:loadDMIStubsTag
// @see ServerObject
// @see DataSource.serverObject
// @see OperationBinding.serverObject
//
// @see group:clientServerIntegration
//
// @title Direct Method Invocation
// @treeLocation Client Reference/RPC
// @requiresModules SCServer
// @visibility external
//<


//> @groupDef applicationDeclaration
// When using the SmartClient server, server side methods written in java can be directly
// invoked from client side code via the +link{DMI.call()} API.
// <P>
// In order to support this an application configuration file needs to be present on your
// server. This file lists out what server side methods are exposed for direct invocation.
// The application configuration file should be named <code><i>appID</i>.app.xml</code> (where
// <i>"appID"</i> is some arbitrary id for your application) and must be present at the
// location specified by the <code>project.apps</code> setting in
// the +link{server_properties,server.properties} file.
// <P>
// The application declaration should be written in xml, and should contain a
// <code>rpcBindings</code> block, which holds +link{serverObject} definitions for each
// exposed method. Here's an example demonstrating the specified format:
// <pre>
//    &lt;Application&gt;
//        &lt;rpcBindings&gt;
//            &lt;ServerObject ID="MathUtil" className="com.example.package.MathUtil"&gt;
//                &lt;visibleMethods&gt;
//                    &lt;method name="addIntegers"/&gt;
//                &lt;/visibleMethods&gt;
//            &lt;/ServerObject&gt;
//        &lt;/rpcBindings&gt;
//    &lt;/Application&gt;
// </pre>
//
// In this example we're exposing a method <i>"addIntegers"</i> on the server side java
// class <code>com.example.package.MathUtil</code>. A developer could then call DMI.call(...)
// on the client side code to invoke this method on the server, and get at the returned value
// in the +link{RPCResponse} passed to the +link{Callbacks.RPCCallback, RPCCallback}. Note that the application
// config file does not explicitly list out a method signature - the appropriate method to
// call is detected automatically based on the parameters passed to DMI.call on the client side.
// <P>
// See the +link{group:dmiOverview,DMI overview} for further information on Direct Method Invocation
// in SmartClient.
//
// @title Application Declaration Files
// @treeLocation Client Reference/RPC
// @visibility external
//<

isc.defineClass("DMI").addClassProperties({

actionURL: isc.RPCManager.actionURL,

//> @classMethod DMI.call()
//
// Calls a server-side DMI method.  At a minimum, you need to specify the appID
// (+link{group:applicationDeclaration,.app.xml file}), +link{serverObject.className}
// or +link{serverObject.ID} and methodName to call.
// Arguments and callback are optional.  There are two ways to invoke this method:
// <pre>
// DMI.call(appID, className, methodName,
//          arg1, arg2 ...argN, callback);
// </pre>
// or:
// <pre>
// DMI.call({
//     appID: appID,
//     className: className,
//     methodName: methodName,
//     arguments: [arg1, arg2, ...argN], //optional
//     callback: callback, //optional
//     requestParams: requestProps // optional
// });
// </pre>
// If you use the first signature, you must either specify a callback or if you don't want a
// callback, pass a <code>null</code> as the last argument.  The second signature allows you to
// specify requestParams that are applied to the +link{RPCRequest} generated by this DMI call.
// This allows you to override some defaults - for example to suppress the "Contacting Server"
// prompt, change it's text; change the timeout or set any other property settable on
// +link{RPCRequest}.
// <p>
// Arguments to be passed to the server method may be specified.
// If present, each argument will be serialized into JSON,
// sent to the server and translated to an equivalent Java object to be passed into the
// method. The translation follows the same rules as for data passed to the server as
// part of a standard +link{RPCRequest.data,RPCRequest}. <br>
// For example if a server side method has the signature
// <pre>
// someMethod(String title, Map overrides);
// </pre>
// a DMI call to that method would could pass in a standard String and JavaScript object -
// for example:
// <pre>
// isc.DMI.call({
//     appID: "someApp",
//     className: "com.smartclient.demo.MyClass",
//     methodName: "someMethod",
//     arguments: ["Title String", {field1:"Value 1", field2:"Value 2"}]
// });
// </pre>
// <p>
// Note that you can use the +link{group:loadDMIStubsTag} tag to bind all methods of
// <code>ServerObjects</code> defined in a given .app.xml file and call methods on them
// directly.
//
// @param appID         (string or Object)  the appID (.app.xml file to look in) or comprehensive request
//                          object as documented above.
// @param className     (string)    +link{serverObject.className} or +link{serverObject.ID}
// @param methodName    (string)    the name of the method to call on the serverObject
// @param [args]        (any)    The next N-1 params specify arguments to the server-side method.
// @param callback      (RPCCallback)    The callback of the response.  If you do not want a callback, you
//                          must specify a <code>null</code> value for this parameter when
//                          using the first signature (documented above).
//
// @return (RPCRequest) the RPCRequest that was sent to the server.
//
// @visibility external
//<
call : function (appID, className, methodName) {

    // arguments isn't a real array so methods like slice() that we use below don't work on it
    // - so make a real array
    var args = [];
    for (var i = 0; i < arguments.length; i++) args[args.length] = arguments[i];

    // two invocation styles: can pass an object literal that is the dmi request or as the
    // documented above for external consumption
    var request = {};
    if (isc.isAn.Object(appID) && args.length == 1) {
        // internal signature
        // appID = {
        //   appID: x,
        //   className: x,
        //   methodName: x,
        //   arguments: [],
        //   callback: x,
        //   requestParams: {}
        // }

        // don't modify user object - clone it
        var requestData = isc.clone(appID);

        if (requestData.requestParams) {
            isc.addProperties(request, this.requestParams, requestData.requestParams);
            delete requestData.requestParams;

            // if downloadResult is true, ensure the file is downloaded via the browser's Save
            // dialog - set transport: "hiddenFrame" and switch off showPrompt
            if (requestData.downloadResult == true) {
                requestData.showPrompt = false;
                requestData.transport = "hiddenFrame";
            }
        }
        request.callback = requestData.callback;
        delete requestData.callback;

        request.data = requestData;
    } else {
        // external style - as in method signature
        // all following args except last = arguments
        // lastArg = callback - must be specified, but can be null
        //
        request.data = {
            appID: appID,
            className: className,
            methodName: methodName,
            // the next argument.lenght-1 args are method arguments
            arguments: args.slice(3, args.length-1)
        };
        // and the last is a callback.
        request.callback = args[args.length-1]
    }

    // force arguments to an array - that's what the server expects
    args = request.data.arguments;
    if (!isc.isAn.Array(args)) {
        if (args == null) args = [];
        else args = [args];
    }
    request.data.arguments = args;

    // mark this as a DMI RPC so the server can figure it out
    request.data.is_ISC_RPC_DMI = true;

    // expose appID, className, methodName in query string.  Useful for looking at timing
    // output of proxy-based performance testing tools like JMeter where parsing the
    // request to get this data is a pain.
    if (this.addMetaDataToQueryString) {
        if (!request.queryParams) request.queryParams = {};
        isc.addProperties(request.queryParams, {
            dmi_appID: request.data.appID,
            dmi_class: request.data.className,
            dmi_method: request.data.methodName
        });
    }

    return isc.RPCManager.sendRequest(request);
},



//> @classMethod DMI.getURL()
//
// Returns a URL to a server-side DMI method.  At a minimum, you need to specify the appID
// (+link{group:applicationDeclaration,.app.xml file}), +link{serverObject.className}
// or +link{serverObject.ID} and methodName to call.
// Arguments are optional.  There are two ways to invoke this method:
// <pre>
// DMI.getURL(appID, className, methodName,
//          arg1, arg2 ...argN);
// </pre>
// or:
// <pre>
// DMI.getURL({
//     appID: appID,
//     className: className,
//     methodName: methodName,
//     arguments: [arg1, arg2, ...argN], //optional
//     requestParams: requestProps // optional
// });
// </pre>
// The second signature allows you to specify requestParams that are applied to the
// +link{RPCRequest} generated by this DMI call.
// <p>
// Note that because the entirety of the request is encoded in the URL, there is an
// inherent limitation on the amount of data that you can send viat he criteria argument to
// the server.  The actual length depends on your server configuration and other factors
// such as the size of cookies (if any) being sent to the server and other HTTP headers in
// use.  Conservatively, assume that you have about 2 kilobytes to work with.
//
// @param appID         (string or Object)  the appID (.app.xml file to look in) or comprehensive request
//                          object as documented above.
// @param className     (string)    +link{serverObject.className} or +link{serverObject.ID}
// @param methodName    (string)    the name of the method to call on the serverObject
// @param [args]        (any)    The next N-1 params specify arguments to the server-side method.
//
// @return (String) a URL that targets the specified DMI
//
// @visibility external
//<
getURL : function (appID, className, methodName) {
    // arguments isn't a real array so methods like slice() that we use below don't work on it
    // - so make a real array
    var args = [];
    for (var i = 0; i < arguments.length; i++) args[args.length] = arguments[i];

    // two invocation styles: can pass an object literal that is the dmi request or as the
    // documented above for external consumption
    var request = {};
    if (isc.isAn.Object(appID) && args.length == 1) {
        // internal signature
        // appID = {
        //   appID: x,
        //   className: x,
        //   methodName: x,
        //   arguments: [],
        //   requestParams: {}
        // }

        // don't modify user object - clone it
        var requestData = isc.clone(appID);

        if (requestData.requestParams) {
            isc.addProperties(request, this.requestParams, requestData.requestParams);
            delete requestData.requestParams;

            // if downloadResult is true, ensure the file is downloaded via the browser's Save
            // dialog - set transport: "hiddenFrame" and switch off showPrompt
            if (requestData.downloadResult == true) {
                requestData.showPrompt = false;
                requestData.transport = "hiddenFrame";
            }
        }
        request.data = requestData;
    } else {
        // external style - as in method signature
        // all following args except last = arguments
        request.data = {
            appID: appID,
            className: className,
            methodName: methodName,
            // the next argument.lenght-1 args are method arguments
            arguments: args.slice(3, args.length)
        };
    }

    // force arguments to an array - that's what the server expects
    args = request.data.arguments;
    if (!isc.isAn.Array(args)) {
        if (args == null) args = [];
        else args = [args];
    }
    request.data.arguments = args;

    // mark this as a DMI RPC so the server can figure it out
    request.data.is_ISC_RPC_DMI = true;

    request._returnStreamFileURL = true;
    request.showPrompt = false;

    return isc.RPCManager.sendRequest(request);
},




//> @groupDef loadDMIStubsTag
//
// <i>produces:</i> JavaScript
// <p>
// Creates global bindings for all serverObjects defined in the <code>rpcBindings</code>
// section of the .app.xml file specified by the <code>ID</code> or <code>name</code> attribute of
// this tag.  Once you've loaded your <code>rpcBindings</code> using this tag, you can call
// methods on the <code>ServerObjects</code> defined there directly.  For example, you can load
// the example.app.xml (located in /shared/app directory of the webRoot of the SDK) like this:
// <pre>
// &lt;isomorphic:loadDMIStubs ID="example"/&gt;
// </pre>
// Whereas using +link{DMI.call} you would have had to invoke the <code>getTimeStamp</code>
// method like this:
// <pre>
// DMI.call("example", "GetTimeStampDMI", "getTimeStamp", new Date(), "alert(data)";
// </pre>
// Having loaded the stubs of the <code>example</code> .app.xml, you can then call
// <code>getTimeStamp</code> like this:
// <pre>
// GetTimeStampDMI.getTimeStamp(new Date(), "alert(data)");
// </pre>
// or this:
// <pre>
// GetTimeStampDMI.getTimeStamp({
//     arguments: [new Date()],
//     callback: "alert(data)"
// });
// </pre>
// or this:
// <pre>
// GetTimeStampDMI.call({
//     methodName: "getTimeStamp",
//     arguments: [new Date()],
//     callback: "alert(data)"
// });
// </pre>
// As with +link{DMI.call}, the last argument must be the callback - if you don't want a
// callback, simply specify <code>null</code> as the callback.  The name of the global binding
// created will be the same as the +link{ServerObject.ID} or the non-qualified name of the
// +link{ServerObject.className} (java namespace, if any, will be stripped).
// <p>
// <b><u>Tag Attributes:</u></b>
// <p>
// <b>ID or name</b><br>
// <i>value format</i>: String - name of .app.xml file to load (minus the .app.xml extension)<br>
// <i>default value</i>: NONE
// <p>
// This attribute specifies the name of the file that contains the rpcBindings to load.
// UI files are located in <code>[webroot]/shared/app</code> by default.  This location is
// changeable in +link{server_properties,[webroot]/WEB-INF/classes/server.properties}
// by setting the config
// parameter <code>project.apps</code> to the directory where your .app.xml files are located.
// We recommend that for prototyping, at least, you use the default directory.
//
// @see DMI
//
// @visibility external
// @requiresModules SCServer
// @treeLocation Java Server Reference/SmartClient JSP Tags
// @title &lt;isomorphic:loadDMIStubs&gt;
//<

// template used to generate method bindings.  firstArg can be an object literal, in which case
// that's passed directly to isc.DMI.call() - otherwise the arguments array is the set of
// arguments to pass to the target method
callTemplate : "(function(){var x = function (firstArg) { "
        // isCall specifies whether this is a generic call() binding - where the methodName
        // must be passed in as the first arg or a named call() binding where the methodName
        // will be encoded into this template
        +"var isCall = ${isCall};"
        +"var obj = {};"
        +"obj.requestParams=this.requestParams;"
        // copy arguments to array - for some reason we can't call standard array methods on
        // the arguments object.
        +"if(isc.isAn.Object(firstArg) && arguments.length == 1){"
        // for isCall == true, the methodName must be supplied in the obj that is the firstArg,
        // so setting it to 'firstArg' here is harmless
//        +"isc.addProperties(obj,{appID:'${appID}',className:'${className}',methodName:'${methodName}',arguments:firstArg.arguments});"
        +"isc.addProperties(obj,{appID:'${appID}',className:'${className}',methodName:'${methodName}'},firstArg);"
        +"} else {"
        +"var args = [];for (var i = 0; i < arguments.length; i++) args[args.length] = arguments[i];"
        // switch on isCall to treat the first argument either as the method name (isCall ==
        // true) or as part of the arguments array
        +"isc.addProperties(obj,{appID:'${appID}',className:'${className}',methodName:isCall?firstArg:'${methodName}',"
                               +"arguments:args.slice(isCall ? 1 : 0,args.length-1),callback:args[args.length-1]});"
        +"}isc.DMI.call(obj);"
        +"};return x;})()",
// returns an object on which you can directly invoke the methods passed in here - those are
// plumbed through to isc.DMI.call()
bind : function (appID, className, methods, requestParams) {
    //!OBFUSCATEOK
    if (!isc.isAn.Array(methods)) methods = [methods];

    // this is the class we'll be returning
    requestParams = isc.addProperties({}, this.requestParams, requestParams)
    var binding = isc.defineClass(className).addClassProperties({
        requestParams : requestParams
    });

    // bind the special 'call' method that works just like DMI.call(), but with the first
    // argument being the methodName to call
    var callArgs = {appID: appID, className: className, methodName: "firstArg", isCall: true};
    binding.call = isc.eval(this.callTemplate.evalDynamicString(this, callArgs));

    // bind all other named methods.  Note that if the user specifies a method named 'call'
    // then it will clobber the generic call binding above
    for (var i = 0; i < methods.length; i++) {
        var bindingArgs = {appID: appID, className: className, methodName: methods[i], isCall: false};
        binding[methods[i]] = isc.eval(this.callTemplate.evalDynamicString(this, bindingArgs));
    }
    window[className] = binding;
    return binding;
},

makeDMIMethod : function (appID, className, isCall, methodName) {
    //!OBFUSCATEOK
    var callArgs = {appID: appID, className: className, isCall: isCall,
                   methodName: isCall ? "firstArg" : methodName};
    return isc.eval(this.callTemplate.evalDynamicString(this, callArgs));
},

// convenience method to adjust server logging levels via DMI calls
_sendBuiltinRPCRequest : function (methodName, callback, arguments) {
    this.call({
        appID: "isc_builtin",
        className: "com.isomorphic.rpc.BuiltinRPC",
        methodName: methodName,
        callback : callback,
        arguments: arguments || [ "SmartClientLog" ],
        requestParams: {
            showPrompt: false,
            willHandleError: true,
            queryParams: { isc_noLog: "1" }
        }
    });
}

});

isc.DMI.callBuiltin = isc.DMI.makeDMIMethod("isc_builtin", "builtin", true);




if (isc.Browser.seleniumPresent && isc.Browser.isFirefox) {

    document.addEventListener("IscSeleniumConfigureServerLogsEvent", function (event) {
        var arguments = event.target.getAttribute("arguments");
        isc.DMI._sendBuiltinRPCRequest("setTemporaryLogThreshold", null,
                                       arguments.split(/,\s*/));
    }, false);

    document.addEventListener("IscSeleniumClearServerLogsEvent", function (event) {
        isc.DMI._sendBuiltinRPCRequest("clearLogEntries");
    }, false);

    document.addEventListener("IscSeleniumRequestServerLogsEvent", function (event) {
        var eventDocument = event.target.ownerDocument;
        isc.DMI._sendBuiltinRPCRequest("getLogEntries", function (rpcResponse) {
            var messages = [],
            data = rpcResponse.data;
            if (data && data.length > 0) messages = data.getProperty("logMessage");

            var answerElement = eventDocument.createElement("IscSeleniumCaptureLogs");
            answerElement.setAttribute("logMessages", messages.join(""));

            eventDocument.documentElement.appendChild(answerElement);

            var capturedEvent = eventDocument.createEvent("HTMLEvents");
            capturedEvent.initEvent("IscSeleniumServerLogsCapturedEvent", true, false);
            answerElement.dispatchEvent(capturedEvent);
        });
        isc.DMI._sendBuiltinRPCRequest("revertTemporaryLogThresholds");
    }, false);

}




//> @type Criteria
// Criteria for selecting only a matching set of records from a DataSource.  Criteria can
// be applied on the client and server.  Unless configured otherwise, criteria will generally
// be applied client-side by +link{ResultSet}s via +link{ResultSet.applyFilter()}.
// <P>
// Client- and server-side systems built into SmartClient understand two criteria formats by
// default: simple key-value pairs (Criteria) or the +link{AdvancedCriteria} format.
// <P>
// <smartclient>
// Simple key-value Criteria are represented via a JavaScript Object where each property
// specifies the name and required value for a field.  Multiple legal values for a field can be
// provided as an Array.  For example:
// <pre>
// var criteria = {
//    field1 : "value1",
//    field2 : ["value2", "value3"]
// }
// </pre>
// Would select all records where field1 has value "value1" and where field2 has <i>either</i>
// "value2" or "value3".
// </smartclient>
// <P>
// Use +link{DataSource.combineCriteria()} to combine two Criteria objects (including Criteria and
// AdvancedCriteria) or +link{DataSource.convertCriteria()} to convert simple Criteria to the
// AdvancedCriteria format.
// <P>
// <smartclient>
// When writing custom client and server-side filtering logic, criteria must be a JavaScript
// Object but the properties of that Object can contain whatever data you want.
// </smartclient>
// When sent to the SmartClient server, the Java representation of the criteria is described
// +link{rpcRequest.data,here}.  When sent to other servers, the
// +link{type:DSProtocol,operationBinding.dataProtocol} affects the format.
//
// @treeLocation Client Reference/Data Binding
// @see CriteriaPolicy
// @visibility external
//<

//>    @class ResultSet
// ResultSets are an implementation of the +link{List} interface that automatically fetches
// DataSource records when items are requested from the List.  ResultSets provide robust,
// customizable, high-performance cache management for ListGrids and other built-in SmartClient
// components, and can be used as cache managers by custom components.
// <P>
// ResultSets manage data paging, that is, loading records in batches as the user navigates
// the data set.  A ResultSet will switch to using client-side sorting and filtering when
// possible to improve responsiveness and reduce server load.  ResultSets also participate in
// automatic cache synchronization, observing operations on DataSources and automatically
// updating their caches.
// <P>
// <b>Creation</b>
// <P>
// A ResultSet can be passed to any component that expects a List, and the List APIs can be
// called directly on the ResultSet as long as the caller is able to deal with asynchronous
// loading; see +link{method:ResultSet.getRange()}.
// <P>
// Generally ResultSets do not need to be created directly, but are created by DataBound
// components as an automatic consequence of calling
// +link{group:dataBoundComponentMethods,DataBound Component Methods}.
// For example, the +link{listGrid.fetchData()} causes +link{listGrid.data} to become an
// automatically created <code>ResultSet</code> object.  Automatically created ResultSets
// can be customized via properties on ListGrids such as +link{listGrid.dataPageSize} and
// +link{listGrid.dataProperties}.  All ResultSets for a given DataSource may also be
// customized via setting +link{dataSource.resultSetClass} to the name of a ResultSet
// +link{classMethod:isc.defineClass(),subclass} in which
// +link{classMethod:class.addProperties,defaults have been changed}.
// <P>
// A ResultSet defaults to using data paging, setting +link{dsRequest.startRow} and
// +link{dsRequest.endRow} in issued dsRequests.  Server code may always return more rows than
// the ResultSet requests and the ResultSet will correctly integrate those rows based on
// +link{dsResponse.startRow}/+link{dsResponse.endRow,endRow}.
// Hence the server can always avoid paging mode by simply returning all matching rows.
// <P>
// A ResultSet can be created directly with just the ID of a +link{DataSource}:
// <pre>
//     isc.ResultSet.create({
//         dataSource : "<i>dataSourceID</i>"
//     })
// </pre>
// <P>
// Directly created ResultSets are typically used by custom components, or as a means of
// managing datasets that will be used by several components.
// <P>
// When created directly rather than via a dataBoundComponent, a newly created ResultSet will
// not issue it's first "fetch" +link{DSRequest} until data is accessed (for example, via
// +link{resultSet.get,get()}).
// <P>
// <b>Paging and total dataset length</b>
// <P>
// When using data paging, the server communicates the total number of records that match the
// current search criteria by setting +link{dsResponse.totalRows}.  The ResultSet will then
// return this number from +link{resultSet.getLength,getLength()}, and ListGrids and other
// components will show a scrollbar that allows the user to jump to the end of the dataset
// directly.
// <P>
// However, the ResultSet does not require that the server calculate the true length of the
// dataset, which can be costly for an extremely large, searchable dataset.  Instead, the
// server <i>may</i> simply advertise a <code>totalRows</code> value that is one page larger
// than the last row loaded.  This results in a UI sometimes called "progressive loading",
// where the user may load more rows by scrolling past the end of the currently loaded rows,
// but is not allowed to skip to the end of the dataset.
// <P>
// No client-side settings are required to enable this mode - it is entirely server-driven.
// However, it is usually coupled with +link{listGrid.canSort,disabling sorting}, since
// server-side sorting would also force the server to traverse the entire dataset.  Note
// also the +link{DataSource.progressiveLoading,progressiveLoading} flag, which can be applied
// at a DataSource, operation, request, component or ResultSet level; if you are using the
// built-in server-side DataSource implementations with Pro or better, this tells SmartClient
// Server to use its pre-built progressive loading mode for that DataSource, operation,
// request, component or ResultSet.
// <P>
// <b>Client-side Sorting and Filtering</b>
// <P>
// If a ResultSet obtains a full cache for the current set of filter criteria, it will
// automatically switch to client-side sorting, and will also use client-side filtering
// if the filter criteria are later changed but appear to be <i>more restrictive</i> than the
// criteria in use when the ResultSet obtained a full cache.
// <P>
// The +link{resultSet.useClientSorting,useClientSorting} and
// +link{resultSet.useClientFiltering,useClientFiltering} flags can be used to disable
// client-side sorting and filtering respectively if these behaviors don't match server-based
// sorting and filtering.  However, because client-side sorting and filtering radically improve
// responsiveness and reduce server load, it is better to customize the ResultSet so that it
// can match server-side sorting and filtering behaviors.
// <P>
// Sorting behavior is primarily customized via the "sort normalizer" passed to
// +link{resultSet.sortByProperty}, either via direct calls on a standalone ResultSet, or via
// +link{listGridField.sortNormalizer} for a ListGrid-managed ResultSet.
// <P>
// By default, client-side filtering interprets the +link{type:Criteria,criteria} passed to
// +link{resultSet.setCriteria,setCriteria()} as a set of field values that records must match
// (similarly to the built-in SQL/Hibernate connectors built into the SmartClient Server).
// Custom client-side filtering logic can be implemented by overriding
// +link{resultSet.applyFilter,applyFilter()}.  Overriding
// +link{resultSet.compareCriteria,compareCriteria()} allows you to control when the ResultSet
// uses client-side vs server-side filtering, and the ResultSet has two default
// +link{resultSet.criteriaPolicy,criteria policies} built-in.
// <P>
// <b>Modifying ResultSets</b>
// <P>
// Records cannot be directly added or removed from a ResultSet via +link{List}
// APIs such as +link{List.removeAt(),removeAt()}, since this would break the consistency of
// server and client row numbering needed for data paging, and also
// create some issues with automatic cache synchronization.
// <P>
// Use +link{dataSource.addData()}/+link{DataSource.removeData(),removeData()} to add/remove
// rows from the +link{DataSource}, and the ResultSet will reflect the changes automatically.
// <P>
// To create a locally modifiable cache of Records from a DataSource, you
// can use +link{dataSource.fetchData()} to retrieve a List of Records which can
// be modified directly, or you can create a client-only +link{DataSource} from
// the retrieved data to share a modifiable cache between several
// DataBoundComponents.
// <P>
// <b>Updates and Automatic Cache Synchronization</b>
// <P>
// Once a ResultSet has retrieved data or has been initialized with data, the ResultSet will observe any
// successful "update", "add" or "remove" dsRequests against their DataSource, regardless of the
// component that initiated them.  A ResultSet with a full cache for the current filter criteria will
// integrate updates into the cache automatically.
// <P>
// Updated rows that no longer match the current filter criteria will be removed
// automatically.  To prevent this, you can set +link{resultSet.neverDropUpdatedRows}.
// Added rows will similarly be added to the cache only if they match current filter criteria.
// <P>
// Note that the client-side filtering described above is also used to determine whether
// updated or added rows should be in the cache.  If any aspect of automated cache update is
// ever incorrect, +link{resultSet.dropCacheOnUpdate,dropCacheOnUpdate} can be set for the
// ResultSet or +link{dsResponse.invalidateCache} can be set for an individual dsResponse.
// <P>
// If automatic cache synchronization isn't working, troubleshoot the problem using the steps
// suggested +externalLink{http://forums.smartclient.com/showthread.php?t=8159#aGrid,in the FAQ}.
// <P>
// <b>Data Paging with partial cache</b>
// <P>
// When in paging mode with a partial cache, a ResultSet relies on server side sorting, setting
// +link{dsRequest.sortBy} to the current sort field and direction.  In order for the cache to
// remain coherent, row numbering must continue to agree between server and client as new
// fetches are issued, otherwise, duplicate rows or missing rows may occur.
// <P>
// If concurrent modifications by other users are allowed, generally the server should set
// +link{dsResponse.invalidateCache} to clear the cache when concurrent modification is
// detected.
// <P>
// In paging mode with a partial cache, any successful "update" or "add" operation may cause
// client and server row numbering to become out of sync.  This happens because the update
// may affect the sort order, and client and server cannot be guaranteed to match for sets of
// records that have equivalent values for the sort field.
// <P>
// For this reason, after an "add" or "update" operation with a partial cache, the ResultSet
// will automatically mark cache for invalidation the next time a fetch operation is performed.
// Alternatively, if +link{resultSet.updatePartialCache} is set to false, the ResultSet will
// simply invalidate cache immediately in this circumstance.
//
// @see interface:DataBoundComponent
// @see group:dataBoundComponentMethods
// @see DataSource.resultSetClass to customize all ResultSets for a given DataSource
// @see resultSet.getRange() for information on handling asynchronous loading
//
// @implements List
// @treeLocation Client Reference/Data Binding
// @visibility external
//<


isc.ClassFactory.defineClass("ResultSet", null, ["List", "DataModel"]);

isc.ResultSet.addClassProperties({
    // Value returned from resultSet.getLength() if the length of the resultSet is not
    // yet known (because we're still fetching data from the server).  Note that the correct
    // way to check for this is resultSet.lengthIsKnown(), not checking for this constant,
    // which could fail.
    UNKNOWN_LENGTH : 1000,




    _simpleProperties: [
        "fetchAhead", "fetchMode", "resultSize", "fetchDelay", "dataSource",
        "criteriaPolicy", "useClientSorting", "useClientFiltering",
        "updateCacheFromRequest", "dropCacheOnUpdate", "dropCacheOnLengthChange",
        "disableCacheSync", "progressiveLoading", "shouldReorderAllRows",
        "neverDropUpdatedRows", "updatePartialCache", "transformUpdateResponses"]
});

isc.ResultSet.addClassMethods({

//> @classMethod resultSet.getLoadingMarker()
// Return the singleton marker object that is used as a placeholder for records that are being
// loaded from the server.
// @return (String) the loading marker
// @visibility external
//<
getLoadingMarker : function () {
    return Array.LOADING;
},

//> @classMethod resultSet._removeNullsAndLoadings()
// This method splices out the sections of the input array that are null, undefined, or are
// equal to the +link{resultSet.getLoadingMarker(),loading marker}.
// @param (array of any) the input array
// @visibility internal
//<
_removeNullsAndLoadings : function (arr) {
    var i = 0, n = 0, len = arr.length,
        loadingMarker = Array.LOADING;

    for (; i < len; ++i) {
        var a = arr[i];
        if (a == null || a === loadingMarker) {
            ++n;
        } else if (n > 0) {
            arr.splice(i - n, n);
            i -= n;
            len -= n;
            n = 0;
        }
    }
    if (n > 0) {
        arr.splice(i - n, n);
    }
    return arr;
},

_prepareSparseData : function (data) {
    var loadingMarker = Array.LOADING,
        onlyLoading = true;
    for (var i = data.length; i--; ) {
        if (data[i] == loadingMarker) {
            data[i] = null;
        } else {
            onlyLoading = false;
        }
    }
    return onlyLoading;
}

});

isc.ResultSet.addProperties({

    addAt : function() {
        isc.logWarn('ResultSets are readonly. This operation (addAt) will be ignored.');
    },
    set : function() {
        isc.logWarn('ResultSets are readonly. This operation (set) will be ignored.');
    },
    removeAt : function() {
        isc.logWarn('ResultSets are readonly. This operation (removeAt) will be ignored.');
    },

    //localData : null, // the cache of rows
    //totalRows : isc.ResultSet.UNKNOWN_LENGTH, // total number of rows in the filtered results
    cachedRows : 0, // numbers of rows we have cached

    // Fetching
    // ----------------------------------------------------------------------------------------

    // whether to fetch rows beyond those requested
    fetchAhead : true,

    //>    @type    FetchMode
    // Mode of fetching records from the server.
    // <P>
    // Generally, "paged" mode should be used unless the maximum number of records is
    // guaranteed to be small.
    //
    // @value "basic" All records that match the current filter are fetched.  Sorting is
    //                performed on the client.
    // @value "paged" Only requested ranges of records are fetched, with predictive fetch
    //                ahead.  Sorting is performed on the server.
    // @value "local" All records available from the DataSource are fetched.  Filtering by
    //                search criteria and sorting are both performed on the client.
    // @group fetching
    // @visibility external
    //<

    //> @attr resultSet.fetchMode (FetchMode : null : IRA)
    // Mode of fetching records from the server. If unset, will default to <code>"local"</code>
    // if +link{resultSet.allRows} is specified, otherwise <code>"paged"</code>.
    //
    // @see type:FetchMode
    // @group fetching
    // @visibility external
    //<
    //fetchMode : "paged",

    //> @attr resultSet.initialData (Array of Record : null : IA)
    // Initial set of data for the ResultSet.
    // <P>
    // This data will be treated exactly as though it were the data returned from the
    // ResultSet's first server fetch.
    // <P>
    // By default, <code>initialData</code> will be considered a complete response
    // (all rows that match the +link{criteria} which the ResultSet was initialized with).
    // <P>
    // Set +link{initialLength} to treat <code>initialData</code> as a partial response,
    // equivalent to receiving a +link{DSResponse} with <code>startRow:0</code>,
    // <code>endRow:initialData.length</code> and <code>totalRows:initialLength</code>.
    // Normal data paging will then occur if data is requested for row indices not filled via
    // <code>initialData</code>.
    // <P>
    // <code>initialData</code> may be provided as a "sparse" array, that is, slots may be left
    // null indicating rows that have not been loaded.  In this way you can create a ResultSet
    // that is missing rows at the beginning of the dataset, but has loaded rows toward the end,
    // so that you can create a component that is scrolled to a particular position of a dataset
    // without loading rows at the beginning.
    //
    // @group fetching, cacheSync
    // @visibility external
    //<

    //> @attr resultSet.initialLength (integer : null : IA)
    // Initial value of the data set length.
    // <P>
    // To create a ResultSet with it's cache partly filled, see +link{initialData}.
    //
    // @group fetching, cacheSync
    // @visibility external
    //<

    //> @attr resultSet.sortSpecifiers (Array of SortSpecifier : null : IA)
    // Initial sort specifiers for a ResultSet. Use +link{resultSet.setSort()} and
    // +link{resultSet.getSort()} to sort the data after initialization rather than
    // attempting to read or modify this property directly.
    // <P>
    // Note: if +link{resultSet.initialData} was specified, the data is assumed to already
    // be sorted to match this sort configuration.
    // @group fetching, cacheSync
    // @visibility external
    //<

    //> @attr resultSet.allRows (Array of Record : null : IRA)
    // If the complete set of records for a resultSet is available when the resultSet is created,
    // it can be made available to the resultSet via this property at initialization time.
    // This data will then be considered cached meaning sorting and filtering can occur on
    // the client (no need for server fetch).
    // This cached data can be dropped via a call to +link{resultSet.invalidateCache()}.
    // @group fetching, cacheSync
    // @visibility external
    //<

    //> @attr resultSet.resultSize (integer : 75 : IRWA)
    // How many rows to retrieve at once.
    // <P>
    // Applicable only with <code>fetchMode: "paged"</code>.  When a paged ResultSet is asked
    // for rows that have not yet been loaded, it will fetch adjacent rows that are likely to
    // be required soon, in batches of this size.
    //
    // @group fetching
    // @visibility external
    //<
    resultSize : 75,

    //> @attr resultSet.fetchDelay (integer : 0 : IRWA)
    // Delay in milliseconds before fetching rows.
    // <P>
    // When a get() or getRange() call asked for rows that haven't been loaded, the
    // ResultSet will wait before actually triggering the request.  If, during the delay, more
    // get() or getRange() calls are made for missing rows, the final fetch to the server will
    // reflect the most recently requested rows.
    // <P>
    // The intent of this delay is to avoid triggering many unnecessary fetches during
    // drag-scrolling and similar user interactions.
    //
    // @group fetching
    // @visibility external
    //<
    fetchDelay : 0,

    // DataModel
    // ---------------------------------------------------------------------------------------
    //> @attr resultSet.dataSource (DataSource : null : IR)
    //  What +link{class:DataSource} is this resultSet associated with?
    // @include dataBoundComponent.dataSource
    // @visibility external
    //<

    //> @attr resultSet.fetchOperation (String : null : IR)
    // @include dataBoundComponent.fetchOperation
    // @visibility external
    //<

    //> @attr resultSet.context (DSRequest Properties : null : IRA)
    // Request properties for all operations performed by this ResultSet
    //<

    //> @type CriteriaPolicy
    // @value "dropOnChange"        Cache is dropped whenever criteria changes.
    // @value "dropOnShortening"    Cache is retained as long as the only changes to criteria
    //                              make the criteria more restrictive as determined by
    //                              +link{ResultSet.compareCriteria()}.
    // @visibility external
    //<

    //> @attr resultSet.criteria (Criteria : null : IRW)
    // Filter criteria used whenever records are retrieved.
    // <P>
    // Use +link{setCriteria()} to change the criteria after initialization.
    // @visibility external
    //<

    //> @attr resultSet.criteriaPolicy (CriteriaPolicy : null : IRWA)
    // Decides under what conditions the cache should be dropped when the +link{criteria}
    // changes.
    // @see criteria
    // @see dataSource.criteriaPolicy
    // @visibility external
    //<

    // Local Operations
    // ----------------------------------------------------------------------------------------

    //> @attr resultSet.useClientSorting (boolean : true : IRWA)
    // Whether to sort data locally when all records matching the current criteria have been
    // cached.
    // <P>
    // This may need to be disabled if client-side sort order differs from server-side sort
    // order in a way that affects functionality or is surprising.
    //
    // @visibility external
    //<
    useClientSorting: true,
    shouldUseClientSorting : function () {
        //>Offline
        if (!isc.RPCManager.onLine) return true;
        //<Offline
        return this.useClientSorting;
    },

    //> @attr resultSet.useClientFiltering (boolean : true : IRWA)
    // Whether to filter data locally when all DataSource records have been loaded (that is,
    // criteria is blank and cache is complete).
    // <P>
    // This may need to be disabled if client-side filtering differs from server-side filtering
    // in a way that affects functionality or is surprising.
    // <P>
    // This setting is distinct from <code>fetchMode:"local"</code>, which explicitly loads all
    // available DataSource records up front and always performs all filtering on the client.
    // <P>
    // See +link{resultSet.applyFilter()} for default filtering behavior.
    // <P>
    // <b>NOTE:</b> even with useClientFiltering false, client-side filtering will be used
    // during cache sync to determine if an updated or added row matches the current criteria.
    // To avoid relying on client-side filtering in this case, either:<br>
    // - avoid returning update data when the updated row doesn't match the current filter<br>
    // - set dropCacheOnUpdate<br>
    //
    // @visibility external
    //<
    useClientFiltering:true,
    shouldUseClientFiltering : function () {
        //>Offline
        if (!isc.RPCManager.onLine) return true;
        //<Offline
        return this.useClientFiltering;
    },

    // Caching
    // ----------------------------------------------------------------------------------------

    //> @attr resultSet.updateCacheFromRequest (boolean : true : IRA)
    // When a successful Add, Update or Remove type operation fires on this ResultSet's
    // dataSource, if +link{dsResponse.data} is unset, should we integrate the submitted
    // data values (from the request) into our data-set? This attribute will be passed to
    // +link{dataSource.getUpdatedData()} as the <code>useDataFromRequest</code> parameter.
    //
    // @group cacheSync
    // @visibility external
    //<
    updateCacheFromRequest:true,

    //> @attr resultSet.dropCacheOnUpdate (boolean : false : IRA)
    // Whether to discard all cached rows when a modification operation (add, update, remove)
    // occurs on the ResultSet's DataSource.
    // <P>
    // A ResultSet that has a complete cache for the current filter criteria can potentially
    // incorporate a newly created or updated row based on the data that the server returns
    // when a modification operation completes.  However this is not always possible for
    // ResultSets that show some types of joins, or when the server cannot easily return update
    // data.  In this case set <code>dropCacheOnUpdate</code> to cause the cache to be
    // discarded when an update occurs.
    // <P>
    // <code>dropCacheOnUpdate</code> can be set either directly on a ResultSet, or on a
    // DataSource in order to affect all ResultSets on that DataSource.
    //
    // @group cacheSync
    // @visibility external
    //<

    //> @attr resultSet.dropCacheOnLengthChange (boolean : true : IRA)
    // Whether to discard all cached rows when the server reports a change in the number of
    // total rows.
    // @group cacheSync
    // @visibility internal
    //<
    // Not yet implemented:
    // <P>
    // This works as a simple form of cache staleness detection if the server is not capable of
    // supporting the more sophisticated <code>cacheTimestamp</code> mechanism.
    //
    // @see attr dsResponse.cacheTimestamp

    //> @attr   resultSet.disableCacheSync (boolean : false : IRA)
    // By default when the data of this ResultSet's dataSource is modified, the ResultSet will
    // be updated to display these changes.
    // Set this flag to true to disable this behavior.
    // @group cacheSync
    // @visibility external
    //<
    // Note: This can be set to false after init, but if already false, setting to true would
    // lead to unpredictable results as we'd be attempting to integrate changes into a possibly
    // out of date cache


    //> @attr resultSet.progressiveLoading (boolean : null : IRW)
    // Sets +link{DataSource.progressiveLoading,progressive loading mode} for this ResultSet.
    // Any +link{DSRequest}s issued by this ResultSet will copy this setting onto the request,
    // overriding the OperationBinding- and DataSource-level settings.
    // <p>
    // This setting is applied automatically by +link{DataBoundComponent}s that have their
    // own explicit setting for +link{DataBoundComponent.progressiveLoading,progressiveLoading}
    //
    // @see dataSource.progressiveLoading
    // @see operationBinding.progressiveLoading
    // @see dsRequest.progressiveLoading
    // @see dataBoundComponent.progressiveLoading
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr resultSet.shouldReorderAllRows (boolean : true : IRW)
    // When true, apply ordering changes that occur in +link{resultSet.localData}
    // to the cache of records +link{resultSet.allRows}, so that events which
    // cause a refresh from the cache don't destroy an ordering applied to
    // +link{resultSet.localData}.  Only has an impact if +link{resultSet.allRowsCached()}
    // returns true since that's when +link{result.allRows} is a meaningful cache.
    //<
    shouldReorderAllRows: true,

    //> @attr   resultSet.neverDropUpdatedRows (boolean : false : IRA)
    // By default when a row is returned by the server, the current +link{setCriteria,filter
    // criteria} are applied to it, and it may disappear from the cache.
    // <P>
    // Set this flag to true to disable this behavior.
    // @group cacheSync
    // @visibility external
    //<
    shouldNeverDropUpdatedRows : function () {
        //>Offline
        if (!isc.RPCManager.onLine) return true;
        //<Offline
        return this.neverDropUpdatedRows;
    },

    //> @attr   resultSet.updatePartialCache (boolean : true : IRA)
    // If set to true, updated and added rows will be integrated into the client-side cache
    // even if paging is enabled and cache is partial.  If <code>updatePartialCache</code> is
    // false, the cache will be invalidated and new data fetched.
    // <P>
    // If updatePartialCache is enabled and an "add" or "update" operation succeeds with a partial
    // cache:
    // <ul>
    // <li> updated rows will remain in their current position.  No attempt will be made to sort
    // them into a new position even if the sort field was updated.
    // <li> newly added rows will be added at either the end (first preference) or beginning of
    // the dataset if that part of the dataset is cached and was most recently requested.
    // If not, the new row is added at the end of the most recently requested contiguously
    // cached range.
    // </ul>
    // The cache will then be dropped the next time rows are fetched, to prevent problems with
    // inconsistent row numbering between the server and client, which could otherwise lead to
    // duplicate rows or rows being skipped entirely.
    //
    // @group cacheSync
    // @visibility external
    //<

    updatePartialCache:true,
    shouldUpdatePartialCache : function () {
        //>Offline
        if (!isc.RPCManager.onLine) return true;
        //<Offline
        return this.updatePartialCache;
    },

    //> @attr   resultSet.transformUpdateResponses (boolean : null : IRA)
    // If true (or null), passes the record(s) returned by an update, add or remove operation
    // through the +link{transformData(),transformData() method}, if one is defined.  If this
    // property is set to false, transformData() is bypassed.
    // <P>
    // Generally, you will want to transform update responses.  This property is provided for
    // reasons of backwards compatibility.
    // @group cacheSync
    // @visibility internal
    //<

    //> @attr   resultSet.alwaysRequestVisibleRows (boolean : false : IRA)
    // If true, records requested only for visible area.
    // @visibility external
    //<
    alwaysRequestVisibleRows: false
});

isc.ResultSet.addMethods({

init : function () {

    // get a global ID so we can be called in the global scope
    isc.ClassFactory.addGlobalID(this);
    //>!BackCompat 2004.7.30
    // custom operation for fetching passed in as just "operation"
    if (this.operation != null) this.fetchOperation = this.operation;
    //<!BackCompat

    // get the fetchOperation since several ResultSet-related settings are legal on it.
    // NOTE: order depedency: if we were passed an operation, getOperation() will return it
    // without looking at this.dataSource, so we can use that operation to automatically derive
    // the dataSource.  Otherwise, if not passed an operation, we require this.dataSource, and
    // getOperation will derive an operation from it.
    var fetchOperation = this.getOperation("fetch");
    // if fetchOperation is an explicitly defined operation, operation.dataSource may be a list
    // of DataSources
    var dsNames = fetchOperation.dataSource;
    if (!isc.isAn.Array(dsNames)) dsNames = [dsNames];
    for (var i = 0; i < dsNames.length; i++) {
        var ds = isc.DS.get(dsNames[i]);
        // observe dataChanged for cache synch
        this.observe(ds, "dataChanged", "observer.dataSourceDataChanged(dsRequest,dsResponse)");

        // keep track of the datasources we've registered with so we can deregister on destroy()
        if (!this._registeredDS) this._registeredDS = [];
        this._registeredDS.add(ds);

        // support automatically deriving the DataSource from the operation (take the first
        // DataSource listed if more than one)
        if (!this.dataSource) this.dataSource = ds;
    }

    if (!this.getDataSource()) {
        this.logError("Invalid dataSource: " + this.echoLeaf(this.dataSource) +
                      ", a ResultSet must be created with a valid DataSource");
    }

    // context.dataPageSize may be set if specified on a DataBoundComponent that created us
    var context = this.context;
    this.resultSize = (context && context.dataPageSize != null ?
                        context.dataPageSize : this.resultSize);

    if (this.allRows) {
        // complete dataset provided, use local filter and sort
        this.fetchMode = "local";
    } else {
        // respect component settings, defaulting to paged
        this.fetchMode = (context && context.dataFetchMode != null ?
                          context.dataFetchMode : this.fetchMode || "paged");
    }

    // whether to invalidate our cache when an update occurs on one of our datasources.
    // Default is update the current cache in place.
    if (this.dropCacheOnUpdate == null) {
        this.dropCacheOnUpdate = this._firstNonNull(fetchOperation.dropCacheOnUpdate,
                                                    this.getDataSource().dropCacheOnUpdate);
    }

    this.context = this.context || {};





    this._loadingRanges = [];

    // backcompat for old name for criteria: "filter"
    var newCriteria = this.criteria || this.filter || {};

    if (this._duplicatingResultSet) {

        this.criteria = newCriteria;
        this._emptyCriteria = (isc.getKeys(newCriteria).length == 0);
    } else {
        // Calling setCriteria() will set up this.criteria and call this.filterLocalData() if
        // we were seeded with 'allRows' (causing this.localData to get set up).
        this.criteria = null;
        this.setCriteria(newCriteria);
    }

    // support for seeding a ResultSet with data on init
    if (this.initialData) {
        isc.ResultSet._prepareSparseData(this.initialData);
        this.fillCacheData(this.initialData);
        this.setFullLength(this.initialLength || this.totalRows || this.initialData.length);
        // allow an initial sort direction to specified (in either format)
        if (this.sortSpecifiers) this.setSort(this.sortSpecifiers, true);
        if (this.sortBy) this.setSort(isc.DS.getSortSpecifiers(this.sortBy), true);
    } else if (this.isPaged()) {
        this._setLocalData([]);
    }


    //>Offline
    this.observe(isc, "goOffline", function () {
        this.goOffline();
    });
    this.observe(isc.RPCManager, "offlineTransactionPlaybackComplete", function () {
        this.offlinePlaybackComplete();
    });
    //<Offline
},

//>Offline
goOffline : function () {

},

offlinePlaybackComplete : function () {
    if (this.haveOfflineRecords) {
        this.invalidateCache();
        this.haveOfflineRecords = false;
    }
},
//<Offline

// de-register from related DataSources on destroy() to prevent leaks
destroy : function () {

    // remove the window.ID pointer to us.  NOTE: don't destroy the global variable if it no longer
    // points to this instance (this might happen if you create a new instance with the same ID)
    if (window[this.ID] == this) window[this.ID] = null;
    //>Offline
    this.ignore(isc, "goOffline");
    this.ignore(isc.RPCManager, "offlineTransactionPlaybackComplete");
    //<Offline

    if (this._registeredDS != null) {
        for (var i = 0; i < this._registeredDS.length; i++) {
            var ds = this._registeredDS[i];
            if (ds) {
                // clear up observations
                this.ignore(ds, "dataChanged");
            }
        }
    }
    this.Super("destroy", arguments);
},

// This method is used by GridRenderer.
getFirstUsedIndex : function () {
    if (!this.lengthIsKnown()) {
        // We're in the middle of loading data.
        return 0;
    } else {

        var localData = this.localData,
            loadingRanges = this._loadingRanges,
            numLoadingRanges = loadingRanges.length,
            k = this._getLoadingRangesIndex(0),
            isNotLoading = (k % 2 != 0),
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        for (var i = 0, localDataLength = localData.length; i < localDataLength; ++i) {
            if (i == j) {
                isNotLoading = !isNotLoading;
                j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
            }
            if (localData[i] != null && isNotLoading) {
                return i;
            }
        }
        return 0;
    }
},

isPaged : function () { return this.fetchMode == "paged" },
isLocal : function () { return this.fetchMode == "local" },
//> @method ResultSet.allMatchingRowsCached() [A]
// Do we have a complete client-side cache of records for the current filter criteria?
// <P>
// Returns <code>false</code> if this is a paged data set, and the entire set of records that
// match the current criteria has not been retrieved from the server. In other words, a
// return value of <code>false</code> means that this <code>ResultSet</code> has a partial cache.
//
// @return (boolean) whether all matching rows are cached
// @visibility external
//<
allMatchingRowsCached : function () {
    // allRows != null and no, or empty allRowsCriteria implies we have a complete cache
    // (This can occur with this.localData being empty during init before setCriteria() has
    // called filterLocalData and set up this.localData)
    //
    // Otherwise look at this.localData - check if data has been loaded and (if paged)
    // cache is full for current filter
    return (this.allRows != null && (!this.allRowsCriteria || this._emptyAllRowsCriteria)) ||
            (this.localData != null &&
            (!this.isPaged() ||

             (this.allRows != null ||(this.cachedRows == this.totalRows))));
},

//> @method ResultSet.allRowsCached() [A]
// Do we have a complete client-side cache of all records for this DataSource?
// <P>
// Becomes true only when the ResultSet obtains a complete cache after a fetch with empty
// criteria.
//
// @return (boolean) whether all rows are cached
//
// @visibility external
//<
allRowsCached : function () {
    return (
            // - in fetchMode:"local" (load all data up front), data has been successfully
            //   loaded

            (this.allRows != null && (!this.allRowsCriteria || this._emptyAllRowsCriteria))
            ||
            // - in other modes, we've detected emptyCriteria and full cache
            (this.allMatchingRowsCached() && this._emptyCriteria)
        );
},
isEmpty : function () {
    if (this.isPaged()) {
        // If there's a full cache for the current filter criteria, check the length of the data
        if (this.allMatchingRowsCached()) {
            return this.getLength() == 0;
        // For a paged dataSet, the cachedRows attribute indicates we have successfully
        // fetched rows from the server (so this is non empty)
        } else if (this.cachedRows > 0) return false;
    }

    return !this.lengthIsKnown() || this.getLength() <= 0;
},
canSortOnClient : function () {
    return this.shouldUseClientSorting() && (this.allMatchingRowsCached()||
                    (isc.Offline && isc.Offline.isOffline()));
},
canFilterOnClient : function () { return this.shouldUseClientFiltering() && this.allRowsCached() },

//> @method resultSet.getValueMap()
// Get a map of the form <code>{ item[idField] -&gt; item[displayField] }</code>, for all
// items in the list.  If more than one item has the same <code>idProperty</code>,
// the value for the later item in the list will clobber the value for the earlier item.
// <P>
// If this method is called when the +link{allMatchingRowsCached(),cache is incomplete}, it
// will trigger fetches, and will return a valueMap reflecting only the currently loaded rows.
//
// @param idField (string)  Property to use as ID (data value) in the valueMap
// @param displayField (string) Property to use as a display value in the valueMap
// @return (object) valueMap object
// @see resultSet.allMatchingRowsCached()
// @visibility external
//<
// picked up as part of the list interface


// List API
// --------------------------------------------------------------------------------------------

//> @method resultSet.getLength()
// Return the total number of records that match the current filter criteria.
// <P>
// This length can only be known, even approximately, when the first results are retrieved from
// the server.  Before then, the ResultSet returns a large length in order to encourage viewers
// to ask for rows.  +link{lengthIsKnown(),ResultSet.lengthIsKnown()} can be called to
// determine whether an actual length is known.
//
// @include List.getLength()
// @visibility external
//<

getLength : function () {

    var unknownLength = this.unknownLength || isc.ResultSet.UNKNOWN_LENGTH;
    if (!this.lengthIsKnown()) return unknownLength;
    // NOTE: when paged, if we obtain a full cache with empty criteria, we set allRows to the
    // full cache and go into local filtering mode (if enabled).  From then on, totalRows,
    // normally set based on server responses, is no longer up to date.
    return (this.isPaged() && !this.allRows ? this.totalRows
                                            : this.localData.length);
},

_getCachedLength : function () {
    if (this.lengthIsKnown()) {
        return this.getLength();
    } else if (this.localData != null) {
        return this.localData.length;
    } else {
        return 0;
    }
},


_getCachedRows : function () {
    return this.cachedRows;
},

//> @method resultSet.indexOf()
// Return the position in the list of the first instance of the specified object.
// <p>
// If pos is specified, starts looking after that position.
// <p>
// Returns -1 if not found.
// <p>
// <b>NOTE:</b> ResultSet.indexOf() only inspects the current cache of records, so it is only
// appropriate for temporary presentation purposes.  For example, it would not be appropriate
// to hold onto a record and attempt to use indexOf() to determine if it had been deleted.
//
// @include List.indexOf()
//<
indexOf : function (item, pos, endPos) {
    if (this.localData == null) return -1;

    // ignore LOADING rows
    if (Array.isLoading(item)) return -1;

    var index = this.localData.indexOf(item, pos, endPos);
    if (index != -1) return index;

    // if not found, try lookup by primary key.  The caller has an object presumably previously
    // retrieved from this ResultSet, but because we drop cached rows in various circumstances,
    // the row may either have fallen out of cache (eg different sort order) or been replaced
    // by a new row with different object identity.
    // NOTE: primarily this is called by Selection and selection-related code, because of it's
    // strategy of putting marker properties onto records.
    return this.getDataSource().findByKeys(item, this.localData, pos, endPos);
},


// implement slideList so that databound resultsets can be reordered.
// Further enhancements include:
// - support unsort(): correctly manage the fact that our order temporarily doesn't reflect
//   current sort
// - support permanent stored orders: if our DS declares that some field represents a permanent
//   stored order, and we are currently sorted by that field, assume the user means to
//   permanently reorder the record, and save changed field numbers
slideList : function (selection, startIndex) {
    if (!this.allMatchingRowsCached() && !this.shouldUpdatePartialCache()) {
        isc.logWarn('updatePartialCache is disabled: record position will not be shifted.');
        return;
    }
    var output = [], i;

    //if destination is negative, set to 0
    if (startIndex < 0) startIndex = 0;

    // Find the index of the first loading row.
    var loadingRanges = this._loadingRanges,
        numLoadingRanges = loadingRanges.length,
        k = this._getLoadingRangesIndex(0),
        isLoading = (k % 2 == 0),
        j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);

    // take all the things from this table before destination that aren't in the list to be moved
    var localData = this.localData;
    for (i = 0; i < startIndex; ++i) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        }
        var record = this.localData[i];

        // bail if we're sliding to/from a loading row
        if (record == null && isLoading) {
            isc.logWarn('Sliding from a row position that has not yet been loaded, ignoring');
            return;
        }
        if (!selection.contains(record)) {
            output.add(record);
        }
    }
    // now put in all the things to be moved
    for (i = 0; i < selection.length; ++i) {
        output.add(selection[i]);
    }
    // now put in all the things after destination that aren't in the list to be moved
    var localDataLength = localData.length;
    for (i = startIndex; i < localDataLength; ++i) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        }
        var record = localData[i];

        if (record == null && isLoading) {
            isc.logWarn('Sliding into a row position that has not yet been loaded, ignoring');
            return;
        }
        if (!selection.contains(record)) {
            output.add(record);
        }
    }
    this._setLocalData(output);

    if (this.shouldUpdatePartialCache()) {
        this.invalidateRowOrder();
    }

    // reorder allRows using localData if property is set
    if (this.shouldReorderAllRows) this.reorderAllRows();

    this.dataChanged();
},

// Rearranges allRows (complete cache of records loaded from the server) to match the ordering
// of the subset of rows that pass our current criteria (localData).  This allows ordering
// reflected in this.localData to persist across events that refresh localData from allRows.
reorderAllRows : function () {

    var i, localData = this.localData;

    if (!this.allRowsCached() || !this.lengthIsKnown()) return;



    // mark the records in this.localData for instant identification below
    for (i = 0; i < localData.length; i++) localData[i]._localDataIndex = i;

    // map the ordering from localData onto matching records in allRows
    var localDataIndex = 0,
        allRows = this.allRows,
        length = allRows.getLength();

    for (i = 0; i < length; i++) {
        var record = allRows.get(i);
        if (record._localDataIndex != null) {
            allRows.set(i, localData[localDataIndex++]);
            delete record._localDataIndex;
        }
    }
},

//> @method resultSet.get()
// Returns the record at the specified position.
// <P>
// All List access methods of the ResultSet have the semantics described in <code>getRange()</code>.
// @include list.get()
// @see getRange()
//<
get : function (pos) {
    if (!isc.isA.Number(pos) || pos < 0) {
        //>DEBUG
        this.logWarn("get: invalid index " + pos);
        //<DEBUG
        return null;
    }

    // optimization: what getRange(pos, pos+1) would do, only we can do it faster: if the
    // requested row is non-null, it's either cached or loading, so return it
    if (this.localData != null) {
        var record = this.localData[pos];
        if (record != null) {
            return record;
        } else if (this._rowIsLoading(pos)) {
            return isc.ResultSet.getLoadingMarker();
        }
    }
    // if this request falls within the rows we are already planning to fetch, likewise return
    // the loading marker (we don't actually put the loading marker into this.localData until
    // the fetch request is sent to the server).

    if (this.fetchStartRow != null && pos >= this.fetchStartRow && pos <= this.fetchEndRow) {
        return Array.LOADING;
    }

    return this.getRange(pos, pos+1)[0];
},

//>    @method resultSet.getRange()
// Return the items between position start and end, non-inclusive at the end, possibly
// containing markers for records that haven't loaded yet.
// <P>
// Calling getRange for records that have not yet loaded will trigger an asynchronous fetch.  The
// returned data will contain the +link{resultSet.getLoadingMarker(),loading marker} as a placeholder
// for records being fetched.  If any rows needed to be fetched, <code>dataArrived()</code> will
// fire when they arrive.
//
// @include list.getRange()
// @see classMethod:getLoadingMarker()
// @see dataArrived()
// @visibility external
//<
getRange : function (start, end, ignoreCache, fetchNow) {
    if (isc._traceMarkers) arguments.__this = this;

    // If end is null, assume its start+1 - just fetch the start row.
    if (start == null) {
        this.logWarn("getRange() called with no specified range - ignoring.");
        return;
    }
    if (end == null) end = start+1;

    if (this.isPaged()) {
        return this._getRangePaged(start, end, ignoreCache, fetchNow);
    }

    if (this.localData == null) {


        this._setLocalData([]);
        // fetch the entire data-set

        this.setRangeLoading(start, end);
        this._fetchAllRemoteData();
    }
    var range = this.localData.slice(start, end);

    // Add in loading markers where appropriate.
    var loadingMarker = isc.ResultSet.getLoadingMarker(),
        loadingRanges = this._loadingRanges,
        numLoadingRanges = loadingRanges.length,
        k = this._getLoadingRangesIndex(start),
        isLoading = (k % 2 == 0),
        j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
    for (var i = start; i < end; ++i) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        }
        if (range[i] == null && isLoading) {
            range[i] = loadingMarker;
        }
    }
    return range;
},

// get the entire set of rows.  Will trigger loading of the entire dataset (minus fully cached
// ranges at the beginning/end)
getAllRows : function () {
    if (!this.lengthIsKnown()) return [];
    return this.getRange(0, this.getLength());
},

// get all rows that are loaded as single Array with no empty slots, even if the loaded rows are in
// several different ranges
getAllLoadedRows : function () {
    if (!this.lengthIsKnown()) return [];
    var rows = [];
    for (var i = 0; i < this.getLength(); i++) {
        if (this.rowIsLoaded(i)) rows.add(this.localData[i]);
    }
    return rows;
},

//> @method resultSet.getAllVisibleRows() [A]
// Returns all rows that match the current criteria.
// <P>
// This method will not trigger a fetch to load more records.  <code>getAllVisibleRows()</code>
// will return null if +link{lengthIsKnown()} is false.
// <P>
// Records are returned in a new List but the Records within it are the same
// instances that the ResultSet is holding onto.  Hence it's safe to add or remove records from
// the List without affecting the ResultSet but modifying the Records themselves is a direct
// modification of the client-side cache.
// @return (Array of Record) the records in the cache that match the current criteria,
// possibly null
// @visibility external
//<
getAllVisibleRows : function () {
    if (!this.lengthIsKnown()) {
        return null;
    } else {
        return isc.ResultSet._removeNullsAndLoadings(this.localData.duplicate());
    }
},

//> @method resultSet.usingFilteredData()
// Determine whether the ResultSet is showing a filtered, proper subset of the cached rows.
// This happens if +link{useClientFiltering,client filtering} is enabled.  Rows may have been
// loaded from the server when a more restrictive criteria is applied such that filtering could
// be performed on the client side.
// <P>
// This method returns false if data is not loaded yet.
// @return (boolean) true if the ResultSet is showing a filtered subset of the cached rows,
// false otherwise.
// @see resultSet.getAllCachedRows
// @visibility external
//<
usingFilteredData : function () {
    return (this.allMatchingRowsCached() && (this.localData.length < this.cachedRows));
},

//> @method resultSet.getAllCachedRows() [A]
// Returns a list of all rows that have been cached.  This is potentially a superset of all rows
// that are available via +link{getAllVisibleRows()} if the ResultSet is using client-side
// filtering to display a subset of loaded rows (see the +link{ResultSet,ResultSet overview}).
// <P>
// If +link{usingFilteredData()} returns false, this is the same list as would be returned by
// +link{getAllVisibleRows()}.
// <P>
// This method will not trigger a fetch to load more records.  getAllCachedRows() will return
// null if +link{lengthIsKnown()} is false.
// <P>
// Records are returned in a new List but the Records within it are the same
// instances that the ResultSet is holding onto.  Hence it's safe to add or remove records from
// the List without affecting the ResultSet but modifying the Records themselves is a direct
// modification of the client-side cache.
// @return (Array of Record) the records in the cache, possibly null
// @visibility external
//<
getAllCachedRows : function () {
    if (!this.lengthIsKnown()) {
        return null;
    } else if (!this.allRows) {
        return this.getAllVisibleRows();
    } else {
        var allRows = this.allRows.duplicate();

        return allRows;
    }
},

// called by grids, allows dynamic derivation of values.  Used in order to allow a ResultSet to
// use a set of XML elements as its dataset.
// "field" is the optional field descriptor used in the visual component.
dynamicDSFieldValues:false,
getFieldValue : function (record, fieldName, field, component, reason) {
    if (this.dynamicDSFieldValues) {
        return this.getDataSource().getFieldValue(record, fieldName, field);
    } else {
        return isc.Canvas._getFieldValue(fieldName, field, record, component, true, reason);
    }
},

//> @method resultSet.duplicate()
// Returns a clone of this ResultSet sharing the same DataSource, current criteria, sort
// direction and cache state.
// <p>
// Internal caches are copied such that methods such as +link{lengthIsKnown()},
// +link{allMatchingRowsCached()} and +link{allRowsCached()} return identical results for the
// new ResultSet.
// <p>
// Subsequent changes to criteria or sort direction on the original ResultSet with not affect
// the duplicate, and vice versa.  Outstanding fetches on the original ResultSet will likewise
// have no effect on the duplicate when they complete.  However, actual Record instances are
// shared between the original and copied ResultSet, so direct modification of Records will
// affect both ResultSets.
// <p>
// The new ResultSet will listen for DataSource changes and automatically update caches just
// as normal ResultSets do.
// <p>
// <b>NOTE:</b> if you are looking for a simple list of Records rather than a new, fully
// functional ResultSet, consider +link{getAllCachedRows()} or +link{getAllVisibleRows()}.  If
// you are looking to create a filterable, sortable subset of the current ResultSet, consider
// creating a new ResultSet and passing the results of <code>getAllCachedRows</code> or
// <code>getAllVisibleRows</code> as the initial value of ResultSet.allRows.
// @visibility external
//<

duplicate : function () {
    // Copy this ResultSet's configuration.  There should be no need to copy over the
    // initialData and initialLength.
    var simpleProperties = isc.ResultSet._simpleProperties,
        config = {};
    for (var i = simpleProperties.length; i--; ) {
        var key = simpleProperties[i];
        config[key] = this[key];
    }
    if (isc.isAn.Array(this.sortSpecifiers)) {
        var ds = this.dataSource,
            sortSpecifiers = config.sortSpecifiers = new Array(this.sortSpecifiers.getLength());
        for (var i = sortSpecifiers.length; i--; ) {
            sortSpecifiers[i] = isc.DataSource.getSortBy(this.sortSpecifiers[i]);
        }
    }
    config.fetchOperation = this.getOperationId("fetch");
    config.context = isc.addProperties({}, this.context);

    // Copy this ResultSet's internal state and data caches.

    var localData = this.localData && this.localData.duplicate(),
        allRows = this.allRows && this.allRows.duplicate();

    config.localData = localData;
    config.totalRows = this.totalRows;
    config.cachedRows = this.cachedRows;
    config.allRows = allRows;
    config.allRowsCriteria = isc.DataSource.copyCriteria(this.allRowsCriteria);
    config.criteria = isc.DataSource.copyCriteria(this.criteria);

    config._duplicatingResultSet = true;
    var duplicate = isc.ResultSet.create(config);
    delete duplicate._duplicatingResultSet;
    return duplicate;
},


// Retrieving rows
// --------------------------------------------------------------------------------------------

_getLoadingRangesIndex : function (rowNum) {
    var loadingRanges = this._loadingRanges;


    var i = 0,
        j = loadingRanges.length - 1;
    while (i + 1 < j) {
        var k = Math.floor((i + j) / 2),
            v = loadingRanges[k];
        if (rowNum < v) {
            j = k;
        } else if (rowNum > v) {
            i = k;
        } else {
            return k;
        }
    }

    if (j == -1 || rowNum < loadingRanges[i]) {
        return i - 1;
    } else if (rowNum == loadingRanges[i] || rowNum < loadingRanges[j]) {
        return i;
    } else {
        return j;
    }
},

_rowIsLoading : function (rowNum) {

    return (this._getLoadingRangesIndex(rowNum) % 2 == 0);
},



_firstLoadingRow : function (startRow, endRow, value) {
    if (!(startRow < endRow)) {
        return -1;
    }
    var loadingRanges = this._loadingRanges,
        j = this._getLoadingRangesIndex(startRow);
    if (value == (j % 2 == 0)) {
        return startRow;
    } else if (j + 1 < loadingRanges.length && loadingRanges[j + 1] < endRow) {
        return loadingRanges[j + 1];
    } else {
        return -1;
    }
},
_lastLoadingRow : function (startRow, endRow, value) {
    if (!(startRow < endRow)) {
        return -1;
    }
    var loadingRanges = this._loadingRanges,
        j = this._getLoadingRangesIndex(endRow - 1);
    if (value == (j % 2 == 0)) {
        return endRow - 1;
    } else if (0 <= j && startRow <= loadingRanges[j] - 1) {
        return loadingRanges[j] - 1;
    } else {
        return -1;
    }
},

_setRangeLoading : function (startRow, endRow, value) {
    if (!(0 <= startRow && startRow < endRow)) {
        return;
    }

    var loadingRanges = this._loadingRanges,
        i = this._getLoadingRangesIndex(startRow),
        j = this._getLoadingRangesIndex(endRow - 1);


    // The terminology used here assumes that value is true.
    var startRowNotLoading = (value != (i % 2 == 0)),
        endRowNotLoading = (value != (j % 2 == 0)),
        mergeLeft = (startRowNotLoading && 0 <= i && startRow == loadingRanges[i]),
        mergeRight = (
            endRowNotLoading &&
            j + 1 < loadingRanges.length &&
            endRow == loadingRanges[j + 1]),
        addStartRow = (!mergeLeft && startRowNotLoading),
        addEndRow = (!mergeRight && endRowNotLoading),
        index = i + (mergeLeft ? 0 : 1),
        howMany = (j - i + (mergeLeft ? 1 : 0) + (mergeRight ? 1 : 0));
    if (addStartRow && addEndRow) {
        loadingRanges.splice(index, howMany, startRow, endRow);
    } else if (addStartRow) {
        loadingRanges.splice(index, howMany, startRow);
    } else if (addEndRow) {
        loadingRanges.splice(index, howMany, endRow);
    } else {
        loadingRanges.splice(index, howMany);
    }

},

//> @method resultSet.lengthIsKnown()
// Whether the ResultSet actually knows how many records are available from the server.
// The ResultSet will not know how many records are available when initially fetching and
// filtering data. Note that the value returned from +link{resultSet.getLength()} will be
// an arbitrary, large value if the actual length is not known.
// @return (boolean) whether length is known
// @visibility external
//<
lengthIsKnown : function () {
    // for a paged RS, totalRows remains null until you call setFullLength()
    // for a local or basic RS we never know the total length until fetch() returns
    return this.localData != null && (this.isPaged() ? this.totalRows != null :
                                                       this._fetchingRequest == null);
},

//> @method resultSet.fetchIsPending()
// Whether a fetch is in progress for the ResultSet.  This includes both fetches already
// sent to the server, and fetches that are merely scheduled for execution on pause.
// @return (boolean) whether a fetch is pending
//<
fetchIsPending : function () {
    return this._fetchingRequest != null || this.pendingActionOnPause("fetchRemoteData");
},

//> @method resultSet.rowIsLoaded() [A]
// Whether the given row has been loaded.
// <p>
// Unlike get(), will not trigger a server fetch.
//
// @param   rowNum  (number)   row to check
// @return (boolean) true if the given row has been loaded, false if it has not been
//                   loaded or is still in the process of being loaded
// @visibility external
//<
rowIsLoaded : function (rowNum) {
    return (this.localData != null && this.localData[rowNum] != null);
},

// internal fast-as-possible row check used in certain inner loops.  Skips check for
// localData != null so caller must check that first via lengthIsKnown()
getCachedRow : function (rowNum) {
    var row = this.localData[rowNum];
    if (row != null) {
        return row;
    } else {
        return null;
    }
},

//> @method resultSet.rangeIsLoaded() [A]
// Whether the given range of rows has been loaded.
//
// Unlike getRange(), will not trigger a server fetch.
//
// @param   startRow (number)   start position, inclusive
// @param   endRow   (number)   end position, exclusive
// @return (boolean) true if all rows in the given range have been loaded, false if any rows in
//                   the range have not been loaded or are still in the process of being loaded
// @visibility external
//<
rangeIsLoaded : function (startRow, endRow) {
    var localData = this.localData;
    if (localData == null) {
        return false;
    }
    for (var i = startRow; i < endRow; i++) {
        if (localData[i] == null) {
            return false;
        }
    }
    return true;
},

// get the index of the last cached row after rowNum, or null if rowNum itself is not cached.
// "reverse" parameter searches backwards
findLastCached : function (rowNum, reverse) {
    if (!this.rowIsLoaded(rowNum)) return null;

    var localData = this.localData;
    if (reverse) {
        var i = rowNum;
        while (i >= 0 && localData[i] != null) {
            --i;
        }
        return i + 1;
    } else {
        var length = this.getLength(),
            i = rowNum;
        while (i < length && localData[i] != null) {
            ++i;
        }
        return i - 1;
    }
},

// get the index of the first and last cached row around rowNum, or null if rowNum itself is
// not cached.
getCachedRange : function (rowNum) {
    // default to the last requested range, or zero
    if (rowNum == null) rowNum = this.lastRangeStart;
    if (rowNum == null) rowNum = 0;
    // no cache around this row
    if (!this.rowIsLoaded(rowNum)) return null;
    var length = this.getLength();
    if (this.allMatchingRowsCached()) return [0, length-1];

    var startIndex = this.findLastCached(rowNum, true),
        endIndex = this.findLastCached(rowNum);

    return [startIndex, endIndex];
},

//>    @method    resultSet.setRangeLoading()
// Initializes null data in the specified range to the
// +link{resultSet.getLoadingMarker(),loading marker}.
//
//        @param    start    (number)    start position
//        @param    end        (number)    end position
// @visibility internal
//<
setRangeLoading : function (start, end) {
    if (this.localData == null) {
        this._setLocalData([]);
    }
    this._setRangeLoading(start, end, true);
},

// given an array, set all null values to the "loading" marker and fill out the array to the
// length specified.
fillRangeLoading : function (arr, length) {

    var loadingMarker = isc.ResultSet.getLoadingMarker();
    for (var i = 0; i < length; i++) {
        if (arr[i] == null) {
            arr[i] = loadingMarker;
        }
    }
    return arr;
},

getServerFilter : function () {
    // local mode: no server-side filtering
    if (this.isLocal()) return null;
    // dup the criteria so if it gets modified downstream EG by transformRequest we don't
    // reflect that here.
    return isc.shallowClone(this.criteria);
},

// clear the fetch timer and explicitly pass start and end row to provide a clean entry point
// for the overrideable fetchRemoteData
_fetchRemoteData : function () {
    var startRow = this.fetchStartRow,
        endRow = this.fetchEndRow;

    // this indicates that a timer was set to do a fetch, but then an immediate fetch (no fetch
    // delay) occurred
    if (startRow == null || endRow == null) return;

    // now that we're definitely going to fetch the data, insert loading markers into the local
    // cache so that we don't issue fetch requests on any rows we're already fetching.
    this.setRangeLoading(startRow, endRow);
    this.fetchStartRow = null;
    this.fetchEndRow = null;

    //>DEBUG
    this.logInfo("fetching rows " + [startRow, endRow] + " from server"); //<DEBUG

    return this.fetchRemoteData(this.getServerFilter(), startRow, endRow);

},

_fetchAllRemoteData : function () {
    this.fetchRemoteData(this.getServerFilter());
},

//>    @method    resultSet.fetchRemoteData() [A]
// Retrieve a range of rows from the server.
// <P>
// Override to provide your own implementation of data retrieval.  Use fillCacheData() and
// setFullLength() to manipulate the cache once rows are retrieved.
// <P>
// Implementer is expected to return <b>all</b> requested rows.  fetchRemoteData() may ask for
// more rows than the current totalRows; if not enough rows are available, setFullLength()
// should be called to trim to the appropriate size.
//
// @see setFullLength()
// @see fillCacheData()
//
//        @param serverCriteria (Criteria)    criteria to be applied by server
//        @param startRow          (number)        startRow position
//        @param endRow          (number)        endRow position
// @visibility customResultSet
//<
_requestIndex:0,
fetchRemoteData : function (serverCriteria, startRow, endRow) {
    if (isc.Offline.isOffline()) {
        // save a marker that we have offline records so we can invalidateCache() when
        // we go online, but continue the fetch anyway because there may be a suitable
        // response in the Offline cache
        this.haveOfflineRecords = true;
    }

    this._requestIndex += 1;

    var requestProperties = isc.addProperties({
        operationId : this.getOperationId("fetch"),
        startRow : startRow,
        endRow : endRow,
        sortBy : isc.shallowClone(this._serverSortBy),
        resultSet : this,
        componentId : this.componentId || "(created directly)",
        componentContext : this.componentContext
    }, this.context);
    var internalClientContext = { requestIndex: this._requestIndex };
    if (this.context && this.context.internalClientContext) {
        internalClientContext = isc.addProperties(
            {}, this.context.internalClientContext, internalClientContext);
    }
    requestProperties.internalClientContext = internalClientContext;

    if (this.progressiveLoading === true || this.progressiveLoading === false) {
        requestProperties.progressiveLoading = this.progressiveLoading;
    }

    // Override willHandleError so we don't get wedged in a loading state
    requestProperties.internalClientContext._explicitWillHandleError = requestProperties.willHandleError;
    requestProperties.willHandleError = true;

    // if cache was partially updated before, invalidate the cache
    if (this.rowOrderInvalid()) {
        this.logInfo("invalidating rows on fetch due to 'add'/'update' operation " +
                     " with updatePartialCache");
        this.invalidateRows();
    }

    if (this.logIsDebugEnabled("fetchTrace")) {
        this.logWarn("ResultSet server fetch with server criteria: " +
                     this.echoFull(serverCriteria) + this.getStackTrace());
    }

    if (this.cachingAllData) requestProperties.cachingAllData = true;

    // For local filtering, we fetch every row in the dataset once (and filter the results)
    // For basic filtering we re-fetch when criteria change.
    // In either case the total number of rows that match the filter criteria will be unknown
    // until the fetch returns.
    // This differs from paged ResultSets where we track total size via the totalRows
    // flag (and a fetch doesn't necessarily mean a complete dataset refresh).
    // Set a flag so lengthIsKnown() can detect when we're loading rows.

    this._fetchingRequest = this._requestIndex;

    // WR - XXX - Debugging cache coherence timing issues
    // isc.logWarn("Actually fetching data for rows " + startRow + " to " + endRow + " for RS " + this.ID);
    this.getDataSource().fetchData(serverCriteria,
                 {caller:this, methodName:"fetchRemoteDataReply"},
                 requestProperties);
},

fetchRemoteDataReply : function (dsResponse, data, request) {
    //!DONTOBFUSCATE
    // We observe this method in pickList.js


    var index = dsResponse.internalClientContext.requestIndex;
    if (!this._lastProcessedResponse) this._lastProcessedResponse = 0;
    if (index != (this._lastProcessedResponse+1) && !dsResponse.isCachedResponse) {
        this.logInfo("server returned out-of-sequence response for fetch remote data request " +
                " - delaying processing: last processed:"+ this._lastProcessedResponse + ", returned:"+ index);
        if (!this._outOfSequenceResponses) this._outOfSequenceResponses = [];
        this._outOfSequenceResponses.add({dsResponse:dsResponse, data:data, request:request});
        return;
    }

    if (this.cachingAllData == true) delete this.cachingAllData;

    // Clear the fetchingRequest flag
    if (this._fetchingRequest == index) delete this._fetchingRequest;

    var newData;
    // if the fetch failed, clear our 'loading' marker, and then send over to RPCManager
    // to do normal error handling
    //
    // Handle responses to requests that were outstanding when the cache was invalidated specially.
    // We don't want to insert this data into our cache.
    var hasError = dsResponse.status < 0,
        requestWasInvalidated = index <= this._invalidatedRequestIndex;
    if (requestWasInvalidated && this.logIsWarnEnabled()) {
        this.logWarn(request,
                     "The ResultSet's cache was invalidated while the following request was outstanding: " +
                     isc.echoAll(request) +
                     ", request data:" + isc.echoAll(request.data));
    }
    if (hasError || requestWasInvalidated || dsResponse.offlineResponse) {
        newData = [];
    } else {
        newData = dsResponse.data
    }

    var numResults = newData.length;


    this.document = dsResponse.document;

    //>DEBUG
    this.logInfo("Received " + numResults + " records from server");
    //<DEBUG

    // if the server did not specify startRow, assume startRow is what
    // was asked for
    if (dsResponse.startRow == null) dsResponse.startRow = request.startRow;

    // if the server did not specify an endRow, assume endRow is startRow + number of
    // records returned.
    if (dsResponse.endRow == null) dsResponse.endRow = dsResponse.startRow + numResults;

    // if the server did not specify totalRows, but the resulting endRow is less than what we
    // asked for, then we know the server has no more rows, clamp totalRows to endRow
    if (dsResponse.totalRows == null && dsResponse.endRow < request.endRow) {
        dsResponse.totalRows = dsResponse.endRow;
    }

    // if the cache was invalidated while the request was outstanding, change the response by
    // clearing out the results. This retains the meaningfulness of the response's status and
    // totalRows, etc., but makes sure that the client will not use data intended for insertion
    // into the cache that was invalidated.
    if (requestWasInvalidated) {
        // move up startRow because there is logic below to clamp totalRows to endRow.
        dsResponse.startRow = dsResponse.endRow;
        // note: isAn.Object() returns true for arrays.
        if (isc.isAn.Object(dsResponse.data)) dsResponse.data = [];
    }

    // opportunity to transform data from the server
    if (this.transformData) {
        var result = this.transformData(newData, dsResponse);
        // handle failure to return the untransformed data
        newData = result != null ? result : newData;
        if (newData.length != numResults) {
            this.logInfo("Transform applied, " + newData.length +
                         " records resulted, from " + dsResponse.startRow +
                         " to " + dsResponse.endRow);
            // update endRow
            dsResponse.endRow = dsResponse.startRow + newData.length;
            // must adjust totalRows here if smaller than endRow - startRow, otherwise clamp
            // below will trim results
            if (dsResponse.totalRows != null && dsResponse.totalRows < dsResponse.endRow) {
                dsResponse.totalRows = dsResponse.endRow;
            }
        }
    }

    if (!isc.isA.List(newData)) {
        this.logWarn("Bad data returned, ignoring: " + this.echo(newData));
        return;
    }

    // If the server returned an endRow past the end of the list, warn and clamp
    // dsResponse.endRow to the end of the list so dataArrived etc are passed the correct
    // info on the last loaded row

    if (dsResponse.totalRows != null && dsResponse.totalRows < dsResponse.endRow) {
        this.logWarn("fetchData callback: dsResponse.endRow set to:" + dsResponse.endRow +
                     ". dsResponse.totalRows set to:" + dsResponse.totalRows +
                     ". endRow cannot exceed total dataset size. " +
                     "Clamping endRow to the end of the dataset (" + dsResponse.totalRows +
                     ").");
        dsResponse.endRow = dsResponse.totalRows;
    }

    // NOTE: transformData is allowed to modify results.startRow/endRow/totalRows
    var startRow = dsResponse.startRow,
        endRow = dsResponse.endRow;

    this._startDataArriving();
    // incorporate new data into the cache if not intended for a cache that was invalidated
    if (!requestWasInvalidated) {
        this._handleNewData(newData, dsResponse);
    }
    // if the status returned < 0, suppress firing the dataArrived handler
    this._doneDataArriving(startRow, endRow, hasError);



    delete this.context.afterFlowCallback;

    this._lastProcessedResponse = index;
    if (this._outOfSequenceResponses && this._outOfSequenceResponses.length > 0) {
        for (var i = 0; i < this._outOfSequenceResponses.length; i++) {
            var reply = this._outOfSequenceResponses[i];
            if (reply == null) continue;

            var requestIndex = reply.dsResponse.internalClientContext.requestIndex;
            if (requestIndex == (this._lastProcessedResponse+1)) {
                this.logInfo("Delayed out of sequence data response being processed now " +
                             requestIndex);
                this._outOfSequenceResponses[i] = null;
                this.fetchRemoteDataReply(reply.dsResponse, reply.data, reply.request);
                break;
            }
        }
    }

    // We overrode willHandleError for the request.
    // Fire standard error handling now unless the original request already suppressed
    // this.
    var willHandleError = request.internalClientContext._explicitWillHandleError;
    if (!willHandleError && hasError) {
        isc.RPCManager._handleError(dsResponse, request)
    }
},

_handleNewData : function (newData, result) {
    if (this.isLocal()) {
        // when we get the complete dataset from the server we hold onto it as "allRows" and
        // set this.localData to a locally filtered subset
        // Don't pass this.criteria into setAllRows - we're caching the complete set of data
        // (which has empty criteria)

        this._loadingRanges = [];
        this._setAllRows(newData.duplicate());
        this.filterLocalData();
        return;
    } else if (!this.isPaged()) {
        this._startChangingData();

        this._loadingRanges = [];
        this._setLocalData(newData.duplicate());
        if (this.canSortOnClient()) {
            // if client sorting is allowed, sort locally, so that the server does not have to
            // send sorted records and sorting doesn't have to match between client and server
            this._doSort();
        }

        // If our criteria is empty, we have all rows cached - in this case store this.allRows
        // so we can perform a local filter on a call to 'setCriteria'.  NOTE: done within
        // fillCacheData for a paged ResultSet
        if (this.allRowsCached()) {
            this._setAllRows(this.localData, this.criteria);

        }

        this._doneChangingData();
        return;
    }

    // paged mode
    var context = result.context;

    this._startChangingData()

    // force cache invalidation
    if (result.invalidateCache) {
        this._invalidateCache();
    } else if (this.dropCacheOnLengthChange && this.lengthIsKnown() && this.totalRows != result.totalRows) {
        // crude staleness detection: change in totalRows


        //>DEBUG
        this.logInfo("totalRows changed from " + this.totalRows + " to " +
                     result.totalRows + ", invalidating cache");
        //<DEBUG
        this._invalidateCache(); // NOTE: doesn't trigger observers yet
    }

    // initialize the cache if we've never loaded rows before
    if (this.localData == null) {
        this._setLocalData([]);
    }

    // trimming length discards any "LOADING" marker rows for rows that don't exist on the
    // server
    this.setFullLength(result.totalRows);

    // add the rows to our cache
    this.fillCacheData(newData, result.startRow);

    // handle server batch size set too small: clear loading markers for the rest of the
    // requested range so that new requests will be fired.

    var localData = this.localData;
    var requestedStart, requestedEnd;
    if (result.context) {
        requestedStart = result.context.startRow;
        requestedEnd = result.context.endRow;
    }
    var foundRequestRange = true;
    if (requestedStart == null || requestedEnd == null) {
        foundRequestRange = false;
        requestedStart = result.startRow;
        requestedEnd = this.totalRows;
    }


    var loadingRanges = this._loadingRanges,
        i0 = requestedStart + newData.length,
        i = requestedEnd;
    if (loadingRanges.length > 0) {
        // Get the start of the next range of records that are loading.
        var k = 2 * Math.ceil(this._getLoadingRangesIndex(i0) / 2);
        // If the first loading marker occurs before `requestedEnd` then set `i` to the index
        // of the loading marker.
        i = Math.min(i, loadingRanges[k]);
    }
    if (i > i0) {
        this.logInfo("Fetch request returned range "
             + [result.startRow,(result.startRow + newData.length)]
             + (foundRequestRange ?
                " differs from requested range "
                 + [requestedStart, requestedEnd] + ". " :
                " but we have subsequent 'loading' markers. ")
             + "Assuming client/server batch size mismatch and clearing loading "
             + "markers greater than " + (result.startRow + newData.length));

        this._setRangeLoading(i0, i, false);
    }

    //>DEBUG
    this.logInfo("cached " + newData.getLength() + " rows, from " +
                 result.startRow + " to " + result.endRow +
                 " (" + this.totalRows + " total rows, " + this.cachedRows + " cached)");
    //<DEBUG

    if (this.allMatchingRowsCached()) {
        if (this.allRowsCached()) {
            this.logInfo("Cache for entire DataSource complete");
        } else {
            this.logInfo("Cache for current criteria complete");
        }
        // sort, because if we just completed a dataset where we had a partial cache before,
        // then the data was previously in a sort order specified by the server
        if (this.canSortOnClient()) this._doSort();
    }

    // call dataChanged in case anyone is observing it
    this._doneChangingData();
},

setContext : function (context) {
    this.context = context;
},

//> @method resultSet.findByKey()
// Attempt to find the record in the resultSet that has a primary key value that matches the
// passed in parameter value. Only the locally cached data will be searched.
// Checks only loaded rows and will not trigger a fetch. Returns null if there is no match,
// data is not loaded, or there is no +link{resultSet.dataSource, dataSource}.
// <p>
// Note, if you pass a simple value to this method, it will be matched against the first
// primaryKey field.  For DataSources with a composite primary key (multiple primaryKey fields),
// pass
// <smartclient>a criteria object containing just your primaryKeys, like this:
// <code>{ firstPkField: "value", secondPkField: 25 }</code>.</smartclient>
// <smartgwt>a Criteria instance containing just primaryKey entries.</smartgwt>
//
// @param keyValue (Object) primary key value to search for
// @return (Record) the record with a matching primary key field, or null if not found
// @visibility external
//<
findByKey : function (keyValue) {
     var ds = isc.DataSource.getDataSource(this.dataSource);
     // make sure a dataSource exists for this resultSet
     if (!ds) return;
     // make sure there is a primary key field and that data is loaded
     if (!ds.getPrimaryKeyField() || !this.lengthIsKnown()) return;

     var keyVals;
     // if keyValue is an object, just pass that to findByKeys
     if (isc.isAn.Object(keyValue)) {
         keyVals = keyValue;
     // otherwise make an object that findByKeys() will understand
     } else {
         keyVals = {};
         keyVals[ds.getPrimaryKeyFieldName()] = keyValue;
     }

     // call findByKeys on DataSource for localData
     var index = ds.findByKeys(keyVals, this.localData);
     if (index != null && index != -1) return this.localData[index];
     else return null;
},

// Criteria
// --------------------------------------------------------------------------------------------

//> @method resultSet.setCriteria()
// Set the filter criteria to use when fetching rows.
// <P>
// Depending on the result of +link{compareCriteria()} and settings for
// +link{useClientFiltering} / +link{fetchMode}, setting criteria may cause a trip to the
// server to get a new set of rows, or may simply cause already-fetched rows to be re-filtered
// according to the new Criteria.  In either case, the dataset length available from
// +link{getLength()} may change and rows will appear at different indices.
// <P>
// Note: for simple Criteria, any field values in the criteria explicitly specified as null
// will be passed to the server.  By default the server then returns only records whose value
// is null for that field. This differs from certain higher level methods such as
// +link{listGrid.fetchData()} which prune null criteria fields before performing a fetch
// operation.
//
// @param newCriteria (Criteria) the filter criteria
// @return (boolean) Returns false if the new criteria match the previous criteria, implying
//    the data is unchanged.
// @visibility external
//<
// An overview on the caching system:
// We have 2 kinds of cache
// - the cache of results that matches the current criteria: this.localData
// - the cache of every record we've been handed by the server: this.allRows
//   * When in local filtering mode, this.allRows is always the entire set of records in the
//     data-set, and all sorting and filtering is local (modifying this.localData)
//   * When in basic filtering mode, we also always populate both caches.
//     On the first fetch we fill both caches with the results returned from the server.
//     On subsequent changes to filter criteria, we will either do a client-only filter and
//     modify the local cache, or perform a new server fetch and update both caches depending
//     on useClientFiltering and whether the new critia are more or less restrictive
//   * In paged mode, when we retrieve data from the server, if it is an incomplete data set,
//     records will be slotted into the local cache. Once we have retrieved all matching records
//     for a set of criteria, these will be stored as this.allRows, and for subsequent fetches
//     we'll filter on the client and modify the local cache only if possible (similar to
//     "basic" mode).
// Note: If the user changes textMatchStyle it can become more restrictive
// (EG from substring to exact match). In this case we can avoid hitting the server, and
// perform a local filter. Note that if this happens the allRows cache will contain more rows
// than it would for the same "allRowsCriteria" with the new text match style, but we always
// apply a local filter to this data so the developer / user should never see these extra rows.
setCriteria : function (newCriteria) {
    // use _willFetchData to determine whether we'll hit the server
    // calling the internal version of this method means we'll get back 'null' if the
    // criteria are unchanged, allowing us to skip the call to filterLocalData();
    if (newCriteria == null) newCriteria = {};
    var requiresFetch = this._willFetchData(newCriteria);

    if (requiresFetch == null) {

        // Catch the case where we were seeded with this.allRows on init but haven't yet
        // set up this.localData
        if (this.localData == null && this.allRows != null) this.filterLocalData();

        //>DEBUG
        this.logInfo("setCriteria: filter criteria unchanged");
        //<DEBUG
        return false;
    }

    // clone the criteria passed in - avoids potential issues where a criteria object is passed in
    // and then modified outside the RS
    // Avoid this with advanced criteria - our filter builder already clones the output
    if (!this.getDataSource().isAdvancedCriteria(newCriteria)) {
        // use clone to deep copy so we duplicate dates, arrays etc
        newCriteria = isc.clone(newCriteria);
    }
    var oldCriteria = this.criteria;
    this.criteria = newCriteria;

    // remember whether criteria are empty
    this._emptyCriteria = (isc.getKeys(newCriteria).length == 0);

    this._textMatchStyle = (this.context && this.context.textMatchStyle) ?
                                this.context.textMatchStyle : null;



    if (requiresFetch) {
        //>DEBUG
        this.logInfo("setCriteria: filter criteria changed, invalidating cache");
        //<DEBUG
        this.invalidateCache();
    } else {
        //>DEBUG
        this.logInfo("setCriteria: filter criteria changed, performing local filtering");
        //<DEBUG

        // If we're going to do a local filter and we don't yet have this.allRows set up,
        // populate it now

        if (this.allRows == null) {
            this._setAllRows(this.localData, oldCriteria);
        }
        this.filterLocalData();
    }
    // this indicates the filter criteria changed
    return true;
},


// Setter for the 'allRows' attribute. This is a (complete) cache of records that
// match the criteria passed in and is the most unrestrictive set of data we've recieved
// from the server.
// Once set we'll use local filtering if shouldUseClientFiltering() returns true and
// criteria are more restrictive than this cache
_setAllRows : function (data, criteria) {
    this.allRows = data;
    this.allRowsCriteria = criteria || {};
    this._emptyAllRowsCriteria = (isc.getKeys(this.allRowsCriteria).length == 0);
},

//>!BackCompat 2004.7.23
setFilter : function (newCriteria) { return this.setCriteria(newCriteria) },
//<!BackCompat


//> @method resultSet.getCriteria()
// Get the current criteria for this ResultSet.
// @return (Criteria) current criteria
// @visibility external
//<
getCriteria : function () { return this.criteria },

//> @method resultSet.compareCriteria()
// Default behavior is to call +link{dataSource.compareCriteria()} to determine whether new
// criteria is guaranteed more restrictive, equivalent to the old criteria, or not guaranteed
// more restrictive, returning 1, 0 or -1 respectively.  See
// +link{dataSource.compareCriteria()} for a full explanation of the default behavior.
// <P>
// Override this method or +link{dataSource.compareCriteria()} to implement your own client-side
// filtering behavior.
//
// @param   newCriteria     (Criteria)  new filter criteria
// @param   oldCriteria     (Criteria)  old filter criteria
// @param   [requestProperties]     (DSRequest Properties)  dataSource request properties
// @param   [policy]        (string)    overrides +link{criteriaPolicy}
// @return  (Number)    0 if the filters are equivalent, 1 if newFilter is guaranteed more
//                      restrictive, and -1 if newFilter is not guaranteed more restrictive
// @see criteriaPolicy
// @visibility external
//<
compareCriteria : function (newCriteria, oldCriteria, requestProperties, policy) {
    return this.getDataSource().compareCriteria(
                newCriteria, oldCriteria,
                requestProperties ? requestProperties : this.context,
                policy ? policy : this.criteriaPolicy);
},

compareTextMatchStyle : function (newStyle, oldStyle) {
    return this.getDataSource().compareTextMatchStyle(newStyle, oldStyle);
},

//> @method resultSet.willFetchData()
// Will changing the criteria for this resultSet require fetching new data from the server,
// or can the new criteria be satisfied from data already cached on the client?<br>
// Second <code>textMatchStyle</code> parameter determines whether a change of text-match style
// will require a server fetch - for example if filter is being changed between
// an exact match (from e.g: +link{ListGrid.fetchData()}) and a substring match
// (from e.g: +link{ListGrid.filterData()}).<br>
// This method can be used to determine whether +link{ListGrid.fetchData()} or
// +link{ListGrid.filterData()} would cause a server side fetch when passed a certain set of
// criteria.
// @param newCriteria (Criteria) new criteria to test.
// @param [textMatchStyle] (TextMatchStyle) New text match style. If not passed, assumes
//      textMatchStyle will not be modified.
// @return (boolean) true if server fetch would be required to satisfy new criteria.
// @visibility external
//<

willFetchData : function (newCriteria, textMatchStyle) {
    return (this._willFetchData(newCriteria, textMatchStyle) == true);
},

// Internal version of willFetchData() which actually determines whether we'll fetch
// The only difference is that if the criteria are unchanged it will return explicit null
// rather than true/false. Called directly by setCriteria() where we care about whether
// new criteria would actually require a local filter, as well as whether we should drop
// cache
_willFetchData : function (newCriteria, textMatchStyle) {
    // if we have *no* local data we know we have to hit the server
    // regardless of the new criteria (we've never fetched and weren't seeded with this.allRows)
    if (this.localData == null && this.allRows == null) return true;

    // Determine if the criteria are unchanged / more or less restrictive
    if (newCriteria == null) newCriteria = {};
    var oldCriteria = this.criteria || {},
        oldTextMatchStyle = this._textMatchStyle,
        ds = this.getDataSource();

    if (textMatchStyle == null) {
        textMatchStyle = (this.context && this.context.textMatchStyle) ?
                                this.context.textMatchStyle : null;
    }

    // are we currently viewing a subset of a larger cache of data?
    var isFilteringLocally = this.allRows && this.shouldUseClientFiltering()
                             && (oldCriteria != this.allRowsCriteria);

    // if old criteria is empty and will be used below, ignore its text match style
    var result = isc.isAn.emptyObject(oldCriteria) && !isFilteringLocally ? 0 :
        this.compareTextMatchStyle(textMatchStyle, oldTextMatchStyle);

    // If text match style is less restrictive, no need to check
    // whether the criteria are more restrictive
    if (result >= 0) {

        // if we have switched into local filtering mode after obtaining a full cache (indicated by
        // allRows being set), check whether the new criteria are more or less restrictive
        // than the criteria in use when we obtained a full cache.  This determines whether we can
        // continue to do local filtering.
        var cacheDataCriteria = isFilteringLocally ? this.allRowsCriteria : oldCriteria;
        // if allRowsCriteria is unset, convert to an empty object so we can compare
        // against the criteria passed in.

        if (cacheDataCriteria == null) cacheDataCriteria = {};

        // If one of the criteria objects is an AdvancedCriteria, convert the other one to
        // enable comparison
        if (ds.isAdvancedCriteria(newCriteria) && !ds.isAdvancedCriteria(cacheDataCriteria)) {
            cacheDataCriteria = isc.DataSource.convertCriteria(cacheDataCriteria, textMatchStyle);
        }
        if (!ds.isAdvancedCriteria(newCriteria) && ds.isAdvancedCriteria(cacheDataCriteria)) {
            newCriteria = isc.DataSource.convertCriteria(newCriteria, textMatchStyle);
        }
        var fetchContext = isc.addProperties({},this.context);
        if (textMatchStyle != null) fetchContext.textMatchStyle = textMatchStyle;
        var criteriaResult = this.compareCriteria(newCriteria, cacheDataCriteria, fetchContext);
        // If the criteria changed, respect whether they are more or less strict
        // Otherwise use the result based on whether the text match style changed (becoming more
        // or less strict).
        if (criteriaResult != 0) result = criteriaResult;
    }

    if (result == 0) {
        // criteria match

        // If we're already viewing a subset of a larger cache we tested against that larger
        // cache -- compare the current criteria against the criteria passed in as well to see
        // if the criteria are actually unchanged as far as the visible data is concerned

        if (isFilteringLocally) {
            if (ds.isAdvancedCriteria(newCriteria) && !ds.isAdvancedCriteria(oldCriteria)) {
                oldCriteria = isc.DataSource.convertCriteria(oldCriteria, textMatchStyle);
            }
            if (this.compareCriteria(newCriteria, oldCriteria) != 0) {
                return false;
            }
        }

        return null;
    } else {
        // If the criteria have changed at all, we know we'll have to hit the server if
        // we don't have a complete cache based on our current criteria.
        if (!this.allMatchingRowsCached()) {
            return true;
        }

        // If this is a local resultSet we will always filter locally once we've got any results
        // (and we just verified we have a complete cache for at least our current criteria)
        if (this.isLocal()) {
            return false;
        }


        // criteria (including textMatchStyle) less restrictive than whatever we have cached,
        // And we know this isn't a local resultset
        // Have to fetch.
        if (result == -1) {
            return true;

        } else if (result == 1) {

            // criteria more restrictive (and we have a complete cache for current criteria)
            // unless useClientFiltering is disabled we can filter locally
            if (this.shouldUseClientFiltering()) {
                return false;
            }

            // shouldUseClientFiltering is false - have to fetch
            return true;

        }
    }
},


// Sorting
// --------------------------------------------------------------------------------------------

//> @method resultSet.sortByProperty()
// Sort this ResultSet by a property of each record.
// <P>
// Sorting is performed on the client for a ResultSet that has a full cache for the current
// filter criteria.  Otherwise, sorting is performed by the server, and changing the sort order
// will invalidate the cache.
// <P>
// <b>NOTE:</b> normalizers are not supported by ResultSets in "paged" mode, although valueMaps
// in the DataSource are respected by the SQLDataSource.
//
// @include List.sortByProperty()
// @visibility external
//<
sortByProperty : function (property, sortDirection, normalizer, context) {
    // If we were passed a null normalizer - use the field type as normalizer instead.
    // (May still be null, in which case array sorting will try to derive from actual elements)
    if (normalizer == null) {
        var field = this.getDataSource().getField(property);
        if (field) normalizer = field.type;
    }

    // If we're already sorted as appropriate, no-op.

    if (this._sortProperty == property && this._sortDirection == sortDirection &&
        this._normalizer == normalizer) return;

    var field;
    // If the sort property has a displayField defined and either no optionDataSource or the
    // same opDS as this data model's DS, sort by the displayField
    if (context) {
        field = context.getField(property);
        if (field && field.displayField && field.sortByDisplayField != false) {
            var opDs;
            if (field.optionDataSource) {
                opDs = isc.DataSource.getDataSource(field.optionDataSource);
            }
            if (!opDs || opDs == isc.DataSource.getDataSource(this.dataSource)) {
                property = field.displayField;
            }
        }
    }

    // remember sort and direction
    this._sortProperty = property;
    this._sortDirection = sortDirection;
    this._normalizer = normalizer;
    this._sortContext = context;

    // set sortBy for any subsequent fetches.
    // - if client sorting has been disabled, the server must sort for us
    // - if we are using paging, the server must sort for us, since server and client sort may
    //   not agree
    //   - XXX we could conditionally turn this off in situations where the cache is going to
    //     be completed when the next set of rows arrives, assuming the cache doesn't get
    //     invalidated
    if (this.isPaged() || !this.shouldUseClientSorting()) {
        this._serverSortBy = (this._sortDirection ? "" : "-") + this._sortProperty;
    }

    // we can sort locally if we have the entire result set *for the current filter* cached
    if (this.canSortOnClient()) {

        this._sortLocalDataByProperties([property], [sortDirection], [normalizer], [context]);
        // make sure we keep allRows in sync with localData when sorting, otherwise there
        // can be a case where a call to filterLocalData() will de-synchronize allRows and
        // localData. Subsequent filters will then be filtering from incorrectly sorted data.
        if (this.allRows && (this.localData !== this.allRows)) {
            this.allRows.sortByProperties([property], [sortDirection], [normalizer], [context]);
        }

        // If this is from a cache update, and we're sorting, avoid passing the updated
        // record info to dataChanged()
        delete this._lastUpdateOperation;
        delete this._lastUpdateData;
        delete this._lastOrigRecord;
        delete this._lastUpdateRow;

        if (!this._isChangingData()) this.dataChanged();
        return;
    }

    this.invalidateCache();
},


//>    @method        resultSet.unsort()    (A)
// Turn sorting off for this result set, indicating that the current sort
// order should be preserved.  Return true if this is supported in this List.
// <P>
// Some implementations may not support this -- they should return false
// to indicate to the caller that sort order must be maintained (eg: in
// the case where sort order is derived from the server, etc).
//
//        @group    sorting
//
//        @return    (boolean)    true == list supports unsorting, false == not supported.
//<
unsort : function () {

    if (!this.allMatchingRowsCached()) return false;

    // inline a call to setSort([])
    this._sortSpecifiers = [];
    this._serverSortBy = [];

    // stop maintaining the sort order
    this._sortProperty = null;

    // unsort the cache Array or it will continue to apply the last sort order to rows added
    // without a specific index
    if (this.localData) this.localData.unsort();
    return true;
},


// for internal callers - sort by current sort property
_doSort : function () {
    var specifiers = this._sortSpecifiers;

    // if we don't have data yet, wait to be asked for data.
    // if we have no sort property, leave dataset in current order
    if (this.localData == null || !specifiers || specifiers.length == 0) return;

    // arrange arrays for passing into sortByProperties
    var properties = [],
        directions = [],
        normalizers = [],
        contexts = [];

    for (var i = 0; i < specifiers.length; i++) {
        var item = specifiers[i];
        properties[i] = item.property;
        directions[i] = Array.shouldSortAscending(item.direction);
        normalizers[i] = item.normalizer;
        contexts[i] = item.context;
    }

    // we can sort locally if we have the entire result set *for the current filter* cached
    if (this.canSortOnClient()) {

        // if we have data, and a sort order, sort it.  Otherwise wait until we have data
        // and/or a sort order.
        if (specifiers && specifiers.length>0) {
            // XXX NOTE: function-based normalizers are not supported by server sort (and can't
            // be).  valueMap-based normalizers are, but currently are not submitted, so only
            // valueMaps defined on server DataSources work.

            //>DEBUG
            this.logInfo("_doSort: sorting on properties [" + properties.join(",") + "] : " +
                    "directions ["+ directions.join(",") + "]" +
                     " : full cache allows local sort");
            //<DEBUG

            this._sortLocalDataByProperties(properties, directions, normalizers, contexts);

            // This method may be called from a cache update in which case
            // we don't need to call dataChanged
            if (!this._isChangingData()) {
                this.dataChanged();
            }
        }
        return;
    }
    //>DEBUG
    this.logInfo("_doSort: sorting on properties [" + properties.join(",") + "] : " +
            "directions ["+ directions.join(",") + "]" +
             " : invalidating cache");
    //<DEBUG

    // paged mode: if sort column/direction changed, clear cache

    this.invalidateCache();
},

// multi-property sorting

//> @method resultSet.getSort()
// Return the current sort-specification for this ResultSet as an Array of +link{SortSpecifier}s.
//
// @return  (Array of SortSpecifier) the list of +link{SortSpecifier}s currently applied to
//  this ResultSet
// @visibility external
//<
getSort : function () {
    return this._sortSpecifiers;
},

//> @method resultSet.setSort()
// Sort this ResultSet by the passed list of +link{SortSpecifier}s.
// <P>
// If the ResultSet is already sorted and this method is called with an identical list of
// specifiers, this method will no-op.  To cause data to be resorted with the same set of
// specifiers, use +link{resultSet.resort, resort()}.
// @visibility external
//<
setSort : function (sortSpecifiers, init) {

    var serverSorts = [],
        sameSorts = [],
        field
    ;
    if (sortSpecifiers == null) sortSpecifiers = [];

    for (var i = 0; i < sortSpecifiers.length; i++) {
        var item = sortSpecifiers[i];
        // If we were passed a null normalizer - use the field type as normalizer instead.
        // (May still be null, in which case array sorting will try to derive from actual elements)
        if (item.normalizer == null) {
            var field = this.getDataSource().getField(item.property);
            if (field) {
                item.normalizer = field.type;
                item._autoNormalizer = item.normalizer;
            }
        }

        // If the sort property has a displayField defined and either no optionDataSource or the
        // same opDS as this data model's DS, sort by the displayField
        //
        // note: sanity check for isA.String() in just in case this property was serialized
        // from sortSpecifiers
        if (item.context && !isc.isA.String(item.context)) {
            var undef,
                dsField = this.getDataSource().getField(item.property),
                field = item.context.getField(item.property) || dsField;

            var displayField = field && field.displayField;
            if (displayField === undef) displayField = dsField && dsField.displayField;
            var sortByDisplayField = field && field.sortByDisplayField;
            if (sortByDisplayField === undef) sortByDisplayField = dsField && dsField.sortByDisplayField;

            if (displayField && (sortByDisplayField != false)) {
                var opDs = (field && field.optionDataSource) ||
                            (dsField && dsField.optionDataSource);
                if (opDs) {
                    opDs = isc.DataSource.getDataSource(field.optionDataSource);
                }

                if (!opDs || opDs == isc.DataSource.getDataSource(this.dataSource)) {
                //if (!field.optionDataSource || opDs == isc.DataSource.getDataSource(this.dataSource)) {
                    this.logInfo("Field:" + field.name + " has displayField:" + displayField +
                        " (with optionDataSource:" + opDs + "). " +
                        "Sorting by displayField. Set field.sortByDisplayField to false to disable this.",
                        "sorting");
                    // store the original fieldName as the owningProperty - used when editing
                    // this sortSpecifier later, in a MultiSort[Panel/Dialog]
                    item.owningField = item.property;
                    item.property = displayField;
                }
            }
        }

        // set sortBy for any subsequent fetches.
        // - if client sorting has been disabled, the server must sort for us
        // - if we are using paging, the server must sort for us, since server and client sort may
        //   not agree
        //   - XXX we could conditionally turn this off in situations where the cache is going to
        //     be completed when the next set of rows arrives, assuming the cache doesn't get
        //     invalidated
        if (this.isPaged() || !this.shouldUseClientSorting()) {

            if (item.property) {
                serverSorts[i] = (Array.shouldSortAscending(item.direction) ? "" : "-") + item.property;
            }
        }

        // if called from resultSet.resort(), don't check for the same specifiers, just rerun
        if (!this._resorting) {
            // If the sort-specs appear the same, remember the same items
            if (this._sortSpecifiers && this._sortSpecifiers.length > 0) {
                var matchProps = {
                    property : item.property,
                    direction : item.direction
                };

                if (item.normalizer != null && item.normalizer != item._autoNormalizer) {
                    matchProps.normalizer = item.normalizer;
                }
                var itemIndex = this._sortSpecifiers.findIndex(matchProps);
                if (itemIndex == i) {
                    sameSorts.add(item);
                }
            }
        }
    }

    // If we're already sorted as appropriate, no-op.
    if ((this._sortSpecifiers ? this._sortSpecifiers.length : 0) == sortSpecifiers.length &&
        sortSpecifiers.length == sameSorts.length)
    {
        return;
    }

    // store both the sortSpecifiers for client sorting and the server sort strings
    this._sortSpecifiers = isc.shallowClone(sortSpecifiers);
    this._serverSortBy = isc.shallowClone(serverSorts);

    if (!init) this._doSort();
},

//> @method resultSet.resort()
// Forcibly resort this ResultSet by the current list of +link{SortSpecifier}s.
// @visibility external
//<
resort : function () {
    this._resorting = true;
    this.setSort(isc.shallowClone(this._sortSpecifiers));
    delete this._resorting;
},

// Handling Updates
// --------------------------------------------------------------------------------------------

// We have various cases where local data manipulation needs to fire dataChanged() to notify
// components that they need to refresh rows, etc.
// We use _startChangingData() / _doneChangingData() [Array / List APIs] to handle threads where
// these manipulations as a result of new data arriving from the server, etc, to avoid firing
// dataChanged multiple times for a single change.

// dataArrived() is a ResultSet specific notification API for new data being introduced into the
// cache (typically from a server fetch)
// the _startDataArriving() / _doneWithDataArriving() methods are analagous to
// _startChaningData() / _doneChaningData() for the dataArrived notification
_startDataArriving : function () {
    var undef;
    if (this._dataArriveFlag === undef) this._dataArriveFlag = 0;
    this._dataArriveFlag++;
},

_doneDataArriving : function (startRow,endRow, suppressHandler) {
    if (--this._dataArriveFlag == 0) {
        if (!suppressHandler && this.dataArrived) this.dataArrived(startRow, endRow);
    }
},

_isDataArriving : function () {
    return (this._dataArriveFlag != null && this._dataArriveFlag > 0);
},



// externally called by DataSource when an update operation succeeds on our datasource
dataSourceDataChanged : function (dsRequest, dsResponse) {
    // Respect the flag to never synch cache
    if (this.disableCacheSync) return;

    //>DEBUG
    if (this.logIsDebugEnabled()) this.logDebug("dataSource data changed firing");
    //<DEBUG

    // If the server failed to return the updated records, and updateCacheFromRequest is true,
    // integrate the submitted values into the cache if the operation was succesful.
    var updateData = this.getDataSource().getUpdatedData(dsRequest, dsResponse,
                                                   this.updateCacheFromRequest);

    // Give transformData an opportunity
    if (this.transformData && this.transformUpdateResponses !== false) {
        var transformedData = this.transformData(updateData, dsResponse);
        updateData = transformedData == null ? updateData : transformedData;
    }

    this.handleUpdate(dsRequest.operationType, updateData, dsResponse.invalidateCache, dsRequest);
},

handleUpdate : function (operationType, updateData, forceCacheInvalidation, dsRequest) {
    if (isc._traceMarkers) arguments.__this = this;

    var message = (this.allMatchingRowsCached() ? ", allMatchingRowsCached true"
                            : (", cached rows: " + this.cachedRows +
                               ", total rows: " + this.totalRows));

    // invalidate the cache if explicitly told to..
    if (this.dropCacheOnUpdate || forceCacheInvalidation ||
        // or if we're unable to safely update the cache in place:
        // - "add" or "update" operation
        (operationType != "remove" &&
         // - cache is partial
         !this.allMatchingRowsCached() &&
         // - not configured to update a partial cache
         !this.shouldUpdatePartialCache()))
    {


        // observers such as a ListGrid will notice this cache invalidation and trigger a fetch
        // for replacement data, as necessary
        this.invalidateCache();
        return;
    }

    this.logInfo("updating cache in place after operationType: " + operationType + message);
    // suppress dataChanged until complete
    this._startChangingData();

    // _lastUpdateData / _lastUpdateOperation are used in the case where we know a single record
    // was modified so can pass info to dataChanged() as params, and skip firing that method
    // if the modified record fell outside our cached data.
    // Only set this up if we're dealing
    // with a single updated record.
    if (!isc.isAn.Array(updateData) || updateData.length == 1) {
        this._lastUpdateOperation = operationType;
        this._lastUpdateData = isc.isAn.Array(updateData) ? updateData[0] : updateData;
    }
    // otherwise, update our cache in place.  Note our cache is filtered, so we may just
    // discard the update if the new row doesn't pass the filter
    this.updateCache(operationType, updateData, dsRequest);
    this._doneChangingData();

    // clear the _lastUpdate... flags so we don't erroneously assume future calls to
    // doneChangingData() came from an update operation
    this._lastUpdateOperation = this._lastUpdateData = null;
},

// notifyOnUnchangedCache: If a dataSource operation updates this resultSet's underlying data set but
// doesn't effect the cache (EG a record is updated which doesn't match filter criteria), should
// dataChanged be fired.
notifyOnUnchangedCache:false,

// Override doneChangingData to pass updated row info to dataChanged if a single record was
// modified

_doneChangingData : function (filterChanged, dataFromCache) {
    // If we're dealing with a single row update we can test for the case where the filtered cache
    // wasn't updated at all (and we don't need to fire dataChanged)
    // Note that _lastUpdateData will only be set if a single row was present in the updateData


    // Specific cases:
    // for delete operations we set the _lastUpdateRow to the original position of the removed
    // record in the cache (if present)
    // if we didn't have the record in the cache (had a partial cache) but it matched filter
    // criteria (the length was affected) this._lastUpdateData was cleared so we won't hit this
    // check - fire dataChanged with no params.
    //
    // for add operations we set the _lastUpdateRow flag if we're adding the record to the cache
    // if it's not present.  (but _lastUpdateData is present implying a single record was
    // returned) we can assert that it didn't match criteria so didn't affect the cache
    //
    // for update operations, if the cache was affected we always set the _lastUpdateRow flag
    // (_lastOrigRecord will only be present if the row was present in the cache before the
    // update)
    //
    // so _lastUpdateData being set, and no _lastUpdateRow implies the update didn't affect our
    // cache
    var unmodifiedCache;
    if (!this.notifyOnUnchangedCache && this._lastUpdateData && this._lastUpdateRow == null) {
        unmodifiedCache = true;
    }
    // sanity check we don't want to pass params if we operated on multiple rows
    var operation, record, row;
    if (!unmodifiedCache && this._lastUpdateData) {
        operation = this._lastUpdateOperation;
        record = this._lastOrigRecord;
        row = this._lastUpdateRow;
            //this.logWarn("single row update - operation:" + operation +
            //    ", rowNum:"+ row + ", orig record:" + this.echo(record) +
            //    ", updated record:"+ this.echo(this.get(row)));
    }

    // decrement the _dataChangeFlag even if the cache isn't modified so we can track when
    // to actually fire dataChanged
    if (--this._dataChangeFlag == 0 && !unmodifiedCache) {
        this.dataChanged(operation, record, row, this._lastUpdateData, filterChanged, dataFromCache);
        // clear all 'single row update' type flags unconditionally once we fire dataChanged
        delete this._lastUpdateOperation;
        delete this._lastOrigRecord;
        delete this._lastUpdateRow;
        delete this._lastUpdateRecord;
        delete this._lastUpdateData
    }
},

// NOTE: when we are updating a full cache for the current filter, this depends on local
// filtering being equivalent to server filtering, otherwise:
// - if the server filter is more constrained (common), the client may retain rows that should
//   disappear (temporary and mostly harmless)
//   - to avoid: set dropCacheOnUpdate OR just ensure server does not return rows that should
//     not appear to user
// - if the server filter is more permissive (rare), the client may drop rows that should appear
//   (alarming: user sees the row he just updated vanish)
//   - to avoid: set dropCacheOnUpdate
//
// 2 caches to consider:
// this.localData represents the current data-set that passes our filter criteria
// this.allRows (if set) represents the entire set of data we've been passed by the server.
// In local filtering mode, or if we've performed a fetch with empty criteria this will be
// the entire data set for the dataSource - otherwise will be the results matching the least
// restrictive filter that has been peformed.
// - See setCriteria() for more on this
//
// Therefore every time we need to integrate modified data into the resultSet
// (removing, adding or updating rows), we need to ensure that if this.allRows is present we
// manipulate that cache, and we also manipulate this.localData.
// We achieve this by:
// - if this.allRows is present, modify that cache of rows, and call filterLocalData() to
//   re-generate this.localData based on the new set of data in that method.
//   ** It might be more efficient to explicitly manipulate both arrays rather than regenerating
//      this.localData
// - if this.allRows is not present, modify this.localData directly to integrate the new data
//   in.
updateCache : function (operationType, updateData, dsRequest) {
    if (updateData == null) return;

    operationType = isc.DS._getStandardOperationType(operationType);

    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    //>DEBUG
    if (this.logIsInfoEnabled()) {
        var compStr = (dsRequest.componentId ? " submitted by '" + dsRequest.componentId + "'" :
                        " (no componentID) ");
        this.logInfo("Updating cache: operationType '" + operationType + "'" + compStr + "," +
                     updateData.length + " rows update data" +
                     (this.logIsDebugEnabled() ?
                      ":\n" + this.echoAll(updateData) : ""));
    } //<DEBUG

    switch (operationType) {
    case "remove":
        this.removeCacheData(updateData, dsRequest);
        break;
    case "add":
        this.addCacheData(updateData, dsRequest);
        break;
    case "replace":
    case "update":
        this.updateCacheData(updateData, dsRequest);
        break;
    }

    // if we did an "add" or "update" against a partial cache, row numbering may be invalid
    // relative to server-sider order.  Set a one-time flag that causes cache to be invalidated
    // the next time fetchRemoteData is called.
    if (this.shouldUpdatePartialCache() && operationType != "remove" &&
        !this.allMatchingRowsCached())
    {
        this.invalidateRowOrder();
    }

    var removed = ((operationType == "remove") ||
                    (operationType == "update" && this._lastUpdateRecord == null));

    // if the "allRows" cache is present it will have been updated by the above logic, and
    // this.localData is stale.  We need to re-derive the set of rows that pass the
    // filter if necessary.  Don't filter if we are configured to neverDropUpdatedRows.

    if (this.allRows && !this.shouldNeverDropUpdatedRows()) {
        this.filterLocalData();
    }
    // If we did a local filter, recalculate which row in the cache was affected.
    // Note _lastUpdateRecord comes from the case where a record is newly added to the cache
    var lastUpdateRecord = this._lastUpdateRecord || this._lastOrigRecord;
    if (!removed && lastUpdateRecord != null) {
        var index = this.indexOf(lastUpdateRecord);
        // This implies that the new record was present in the
        // super cache (allrows) but not in the filtered cache (doesn't meet criteria),
        // so there's no visible change.
        if (index == -1) {
            delete this._lastUpdateRow;
            delete this._lastUpdateRecord;
            delete this._lastOrigRecord;
        } else {
            this._lastUpdateRow = index;
        }
    }
},

updateCacheData : function (updateData, dsRequest) {
    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    // NOTE: if allRows is present we are performing a local filter to display a subset of
    // this set of rows.
    // (This may be the entire dataSet for the dataSource - in which case we're in local mode,
    // or just the matching rows for our less restrictive `allRowsCriteria`.)
    var filteringOnClient = this.allRows != null,
        updateFullLength = (!filteringOnClient && this.isPaged() && this.totalRows != null),
        cache = (filteringOnClient ? this.allRows : this.localData),
        updatedRows = 0, removedRows = 0, addedRows = 0,
        notifyAdd = (cache == this.localData && this._dataAdd != null),
        notifyAdded = (cache == this.localData && this._dataAdded != null),
        notifyRemove = (cache == this.localData && this._dataRemove != null),
        notifyRemoved = (cache == this.localData && this._dataRemoved != null),
        notifySplice = (cache == this.localData && this._dataSplice != null),
        notifySpliced = (cache == this.localData && this._dataSpliced != null);

    if (notifyAdd) {
        isc.Func.replaceWithMethod(this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
    }
    if (notifyAdded) {
        isc.Func.replaceWithMethod(this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
    }
    if (notifyRemove) {
        isc.Func.replaceWithMethod(this, "_dataRemove", isc.ResultSet.getArgString("_dataRemove"));
    }
    if (notifyRemoved) {
        isc.Func.replaceWithMethod(this, "_dataRemoved", isc.ResultSet.getArgString("_dataRemoved"));
    }
    if (notifySplice) {
        isc.Func.replaceWithMethod(this, "_dataSplice", isc.ResultSet.getArgString("_dataSplice"));
    }
    if (notifySpliced) {
        isc.Func.replaceWithMethod(this, "_dataSpliced", isc.ResultSet.getArgString("_dataSpliced"));
    }

    var criteria = (filteringOnClient ? this.allRowsCriteria : this.criteria),
        // Don't drop the updated row if neverDropUpdatedRows
        dontDrop = this.shouldNeverDropUpdatedRows(),
        dataSource = this.getDataSource(),
        keyColumns = dataSource.getPrimaryKeyFields();
    for (var i = 0, updateDataLength = updateData.length; i < updateDataLength; ++i) {

        var updateRow = updateData[i],
            keyValues = isc.applyMask(updateRow, keyColumns);

        // Find the index of the old row.
        var index = dataSource.findByKeys(keyValues, cache);

        // See if the new row matches the filter criteria.
        var matchesFilter = (this.applyFilter([updateRow], criteria, this.context).length > 0);


        if (index == -1) {

            // If we didn't find the record in our cache, check for the case where the server
            // returned a changed (bad) primary key value for an update of an existing record.
            // In this case warn, and clear out the original version of the record.
            var submittedRecord = dsRequest.data;
            if (isc.isAn.Array(submittedRecord)) submittedRecord = submittedRecord[0];
            // dsRequest.data can validly be null if DataSource.updateCaches() has been called
            // to propagate cache updates where there was no dsRequest sent by this browser.
            if (submittedRecord) {
                // pare down to PKs and find in our data-set
                submittedRecord = isc.applyMask(submittedRecord, keyColumns);
                var oldRecordIndex = this.getDataSource().findByKeys(submittedRecord, cache);
                if (oldRecordIndex != -1) {
                    this.logWarn("Update operation - submitted record with primary key value[s]:" +
                                this.echo(submittedRecord) + " returned with modified primary key:" +
                                this.echo(keyValues) + ". This may indicate bad server logic. " + "Updating cache to reflect new primary key.");

                    // remove the old record from our dataSet. If it matches filter, we'll re-add below
                    removedRows++;
                    var oldRecord = cache.get(oldRecordIndex);
                    if (notifyRemove) {
                        this._dataRemove(oldRecord, 1, oldRecordIndex);
                    }
                    cache.removeAt(oldRecordIndex);
                    if (updateFullLength) {
                        --this.totalRows;
                    }
                    --this.cachedRows;
                    if (notifyRemoved) {
                        this._dataRemoved(oldRecord, 1, oldRecordIndex);
                    }

                    // this is a weird case - don't attempt to fire row-specific dataChanged
                    delete this._lastUpdateData;
                }
            }


            if (matchesFilter && cache != null) {
                // We got an updated row that matches our filter criteria but is not in the
                // cache.  This could indicate either:
                // - an "update" operation submitted by a separate grid, form or manual call
                //   to dataSource.updateData(), changing an existing row so that it now
                //   matches our criteria, or
                // - an "update" operation was submitted based on a record we have cached, but
                //   there's a server bug where the server returned the wrong PK, so we
                //   couldn't locate the row.  In this second case we've already removed the
                //   original record, so it's appropriate to re-add the "new" record to the
                //   cache.
                this.logInfo("updated row returned by server doesn't match any cached row, " +
                             " adding as new row.  Primary key values: " + this.echo(keyValues) +
                             ", complete row: " + this.echo(updateRow));
                addedRows++;

                var updateRowIndex = -1;
                if (notifyAdd) {
                    if (cache.sortUnique) {
                        updateRowIndex = cache.lastIndexOf(updateRow);
                    }
                    if (updateRowIndex == -1) {
                        cache.add(updateRow);
                        updateRowIndex = cache.lastIndexOf(updateRow);
                        cache.remove(updateRow);
                    }
                    this._dataAdd(updateRow, 1, updateRowIndex);
                }
                cache.add(updateRow);
                if (updateFullLength) {
                    ++this.totalRows;
                }
                ++this.cachedRows;
                if (notifyAdded) {
                    if (!notifyAdd) {
                        updateRowIndex = cache.lastIndexOf(updateRow);
                    }
                    this._dataAdded(updateRow, 1, updateRowIndex);
                }

                // In this case we are adding a row to the cache.
                // If it's being added to the filtered cache figure out the new rowNum here.
                if (updateDataLength == 1) {
                    // Actually store a pointer to the new row.  If we just added the record
                    // to the allRows cache this can be used to calculate the position within
                    // the local cache when the calling method does a local filter.
                    this._lastUpdateRecord = updateRow;
                    this._lastUpdateRow = cache.length - 1;
                }
            } else {
                // This update applies to a row that is not in our cache *and* doesn't match
                // our criteria---just ignore.
            }
        } else {
            // Found the original row (matching primary keys) in our cache.

            if (updateDataLength == 1) {
                var origRecord = cache.get(index);
                // catch the case where the orig record is present in the allRows cache but doesn't
                // match filter criteria for local data
                if (filteringOnClient) {
                    if (this.applyFilter([origRecord], this.criteria, this.context).length == 0) {
                        origRecord = null;
                    }
                }
                // if the original record was in the (filtered) cache, we'll pass it to
                // dataChanged as a parameter
                this._lastOrigRecord = origRecord;
                // also hang onto the position now. If the record is updated it won't change without
                // a sort, and if it's removed we won't be able to pick it up later
                if (origRecord) {
                    this._lastUpdateRow = this.indexOf(origRecord);
                }
            }

            if (matchesFilter || dontDrop) {
                // update the row in place if any of the following is true:
                // - the updated row passes the current filter
                // - we are configured to never drop updated rows
                // - we're actually updating the complete client-side cache, not just the list
                //   of rows that match the filter
                // Note that we use set() instead of just always removing and adding every time
                // because it maintains the record's index when there's no current sort order
                updatedRows++;
                var oldRow = cache.get(index);
                if (notifySplice) {
                    this._dataSplice(oldRow, 1, index, updateRow, 1);
                }
                cache.set(index, updateRow);
                if (notifySpliced) {
                    this._dataSpliced(oldRow, 1, index, updateRow, 1);
                }
            } else {
                // otherwise, row has been changed so that it does not match filter,
                // remove the old row
                //>DEBUG
                if (this.logIsDebugEnabled()) {
                    this.logDebug("row dropped:\n" + this.echo(updateRow) +
                                 "\ndidn't match filter: " + this.echoFull(criteria));
                }
                //<DEBUG

                removedRows++;
                var oldRow = cache.get(index);
                if (notifyRemove) {
                    this._dataRemove(oldRow, 1, index);
                }
                cache.removeAt(index);
                if (updateFullLength) {
                    --this.totalRows;
                }
                --this.cachedRows;
                if (notifyRemoved) {
                    this._dataRemoved(oldRow, 1, index);
                }
            }
        }
    }

    if (this.logIsDebugEnabled()) {
        this.logDebug("updated cache: "
             + addedRows + " row(s) added, "
             + updatedRows + " row(s) updated, "
             + removedRows + " row(s) removed.");
    }

    // reapply current sort to localData if appropriate

    if (!filteringOnClient &&
        (!this.shouldUpdatePartialCache() || this.allMatchingRowsCached()) &&
        this.canSortOnClient())
    {
        this._doSort();
    }
},

removeCacheData : function (updateData) {
    if (!isc.isAn.Array(updateData)) updateData = [updateData];
    // if we're currently filtering on the client, manipulate this.allRows, and
    // we'll re-derive this.localData later. Otherwise interact directly with this.localData
    var filteringOnClient = this.allRows != null,
        cache = filteringOnClient ? this.allRows : this.localData,
        removedRows = 0;

    // For paged data-sets (without a complete cache), this.totalRows is used to determine
    // how many rows exist. Update full length so that we don't advertise rows that don't exist
    // Not required for local/basic rs where we look at localData.length directly
    var updateFullLength = (cache == this.localData && this.isPaged() && this.totalRows != null);

    // Remove any rows that were present in the cache.
    if (cache != null) {
        var ds = this.getDataSource(),
            notifyRemove = (cache == this.localData && this._dataRemove != null),
            notifyRemoved = (cache == this.localData && this._dataRemoved != null);

        if (notifyRemove) {
            isc.Func.replaceWithMethod(
                this, "_dataRemove", isc.ResultSet.getArgString("_dataRemove"));
        }
        if (notifyRemoved) {
            isc.Func.replaceWithMethod(
                this, "_dataRemoved", isc.ResultSet.getArgString("_dataRemoved"));
        }

        for (var i = 0, updateDataLength = updateData.length; i < updateDataLength; ++i) {
            var index = ds.findByKeys(updateData[i], cache);
            if (index != -1) {
                var origRecord = cache[index];
                if (updateDataLength == 1) {
                    // Only pass the removed row to dataChanged if it was present in the
                    // local (filtered) cache.

                    if (!filteringOnClient ||
                        ds.recordMatchesFilter(origRecord, this.criteria, this.context))
                    {
                        this._lastOrigRecord = origRecord;
                        // If we're not looking at the allRows cache the filtered index may not
                        // equal the position in the cache so use indexOf().
                        this._lastUpdateRow = this.indexOf(this._lastOrigRecord);
                    }
                }

                if (notifyRemove) {
                    this._dataRemove(origRecord, 1, index);
                }
                cache.removeAt(index);
                if (updateFullLength) {
                    --this.totalRows;
                }
                --this.cachedRows;
                if (notifyRemoved) {
                    this._dataRemoved(origRecord, 1, index);
                }
                removedRows++;
            } else {
                // Removal of a record not in our cache.  Can happen if grid.removeData() is
                // called on a grid with different filter criteria.

                // We have a complete cache for our criteria.  Nothing to do.
                if (this.allMatchingRowsCached()) continue;

                // If we have a partial cache, use client-side filtering to determine whether
                // this removal has reduced the number of records that match our criteria
                // (which should affect this.totalRows)
                if (ds.applyFilter([updateData[i]], this.criteria, this.context).length > 0) {
                    if (this.logIsDebugEnabled()) {
                        this.logDebug("removed record matches filter criteria: " +
                                      this.echo(updateData[i]));
                    }
                    // Clear the _updateData so we don't attempt to pass params to the
                    // dataChanged method in this case.
                    // Note that we will still need to fire dataChanged since the length has
                    // changed.
                    if (this._lastOrigRecord == null) delete this._lastUpdateData;
                    removedRows++;
                } else {
                    // Row doesn't match our criteria.  Ignore it.
                    if (this.logIsDebugEnabled()) {
                        this.logIsDebugEnabled(
                            "cache sync ignoring 'remove' operation, removed " +
                            " row doesn't match filter criteria: " + this.echo(updateData[i]));
                    }
                }
            }
        }
    }
},

// add rows that pass filtering, then sort the cache if we're in client-side sort mode
addCacheData : function (newRows) {
    if (newRows == null) return;
    if (!isc.isAn.Array(newRows)) newRows = [newRows];

    // if we have the entire dataset cached (allRows != null), add the new rows to the cache of
    // the entire dataset regardless.  Subsequent client-side filtering will eliminate them if
    // they don't match the filter criteria.
    // Also do not attempt to filter if client-side filtering is disabled.
    var validRows;
    if (this.allRows == null || !this.shouldUseClientFiltering()) {
        validRows = this.applyFilter(newRows, this.criteria, this.context);
    } else {
        validRows = newRows;
        if (this.allRowsCriteria) {
            // call this.applyFilter() rather than ds.applyFilter()
            validRows = this.applyFilter(validRows, this.allRowsCriteria, this.context);
        }
    }
    var numValidRows = validRows.length,
        addingSingleRow = false;
    if (numValidRows != newRows.length) {
        this.logInfo("Adding rows to cache, " + numValidRows + " of " + newRows.length +
                     " rows match filter criteria");
    } else if (numValidRows == 1) {
        // flag that we're adding a single row to the cache (so should pass the
        // rowNum to dataChanged)
        addingSingleRow = true;
    }

    var cache = this.allRows || this.localData;
    if (!cache) return;

    var notifyAdd = (cache == this.localData && this._dataAdd != null),
        notifyAdded = (cache == this.localData && this._dataAdded != null);
    if (notifyAdd) {
        isc.Func.replaceWithMethod(
            this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
    }
    if (notifyAdded) {
        isc.Func.replaceWithMethod(
            this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
    }

    // Whether to update full length so that the new rows show up.
    var updateFullLength = (this.isPaged() && !this.allRows && this.totalRows != null);

    var flag = (!this.allMatchingRowsCached() && this.shouldUpdatePartialCache()),
        range = flag && this.getCachedRange();
    if (range) {
        // Design:
        // - if we are at the end of the dataset add the new row there.  This works nicely
        //   for inline edit of new rows in a paged dataset, which is always at the end of
        //   the dataset.
        // - otherwise, if there is any cache at beginning of the dataset (rowIsLoaded(0)),
        //   add there.  This works nicely for eg a form adding data to a paged dataset where
        //   the user has not scrolled
        // - finally, add at the end of the last requested contiguous cached range.  This
        //   works well for link-based paging and other alternative paging interfaces
        var index;
        if (range[1] == this.getLength()-1 || !this.rowIsLoaded(0)) {
            index = range[1] + 1;
        } else {
            index = 0;
        }

        if (notifyAdd && numValidRows > 0) {
            this._dataAdd(validRows, validRows.length, index);
        }
        cache.addListAt(validRows, index);
        if (updateFullLength) {
            this.totalRows += numValidRows;
        }
        this.cachedRows += numValidRows;
        if (notifyAdded && numValidRows > 0) {
            this._dataAdded(validRows, validRows.length, index);
        }
        // if we re-sort this will be dropped
        // if we perform a local filter this will be updated
        if (addingSingleRow) this._lastUpdateRow = index;
    } else {
        // add at the end - they will be sorted into place
        var index;
        if (notifyAdd || notifyAdded) {
            var sortUnique = cache.sortUnique;
            for (var i = 0, len = numValidRows; i < len; ++i) {

                var newRecord = validRows[i],
                    newRecordIndex = -1;
                if (notifyAdd) {
                    if (sortUnique) {
                        newRecordIndex = cache.lastIndexOf(newRecord);
                    }
                    if (newRecordIndex == -1) {

                        cache.add(newRecord, undefined, true);
                        newRecordIndex = cache.lastIndexOf(newRecord);
                        cache.remove(newRecord);
                    }
                    this._dataAdd(newRecord, 1, newRecordIndex);
                }
                cache.add(newRecord, undefined, true);
                if (updateFullLength) {
                    ++this.totalRows;
                }
                ++this.cachedRows;
                if (notifyAdded) {
                    if (!notifyAdd) {
                        newRecordIndex = cache.lastIndexOf(newRecord);
                    }
                    this._dataAdded(newRecord, 1, newRecordIndex);
                }
                if (addingSingleRow) {
                    if (newRecordIndex == -1) {
                        newRecordIndex = cache.lastIndexOf(newRecord);
                    }
                    index = newRecordIndex;
                }
            }
        } else {
            cache.addList(validRows);
            if (updateFullLength) {
                this.totalRows += numValidRows;
            }
            this.cachedRows += numValidRows;

            if (addingSingleRow) index = cache.lastIndexOf(validRows[0]);
        }
        if (addingSingleRow) {
            this._lastUpdateRow = index;
        }
    }
    // store a pointer to the record as well as the row. This can be used to calculate the
    // index within the local cache if we just added the record to the allRows cache.
    if (this._lastUpdateRow != null) {
        this._lastUpdateRecord = cache[this._lastUpdateRow];
    }

    // if we can sort locally, do so now
    if (this.canSortOnClient()) this._doSort();
},



insertCacheData : function (insertData, position, skipAllRows) {
    if (!isc.isAn.Array(insertData)) insertData = [insertData];

    if (this.allRows && this.allRows != this.localData && !skipAllRows) {
        this.allRows.addListAt(insertData, position);
    }

    // Whether to update full length (so that we don't advertise rows that don't exist).
    var updateFullLength = (this.isPaged() && this.totalRows != null);

    var cache = this.localData;
    if (this._dataAdd != null) {
        isc.Func.replaceWithMethod(
            this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
        this._dataAdd(insertData, insertData.length, position);
    }
    // insert the rows into the cache
    cache.addListAt(insertData, position);
    if (updateFullLength) {
        this.totalRows += insertData.length;
    }
    for (var i = insertData.length; i--; ) {
        if (insertData[i] != null && !Array.isLoading(insertData[i])) {
            ++this.cachedRows;
        }
    }
    if (this._dataAdded != null) {
        isc.Func.replaceWithMethod(
            this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
        this._dataAdded(insertData, insertData.length, position);
    }
},


addEmptyValueRecord : function (fieldName) {

    var allRows = this.allRows,
        localData = this.localData,
        localCache = this.isLocal() ? allRows : localData;

    if (!localCache || localCache.find(fieldName, null)) return false;

    var skipAllRows = !allRows || allRows == localData || allRows.find(fieldName, null),
        emptyRecord = {};

    emptyRecord[fieldName] = null;
    this.insertCacheData(emptyRecord, 0, skipAllRows);
    return true;
},


// Notifications After Handling Updates
// --------------------------------------------------------------------------------------------


_setLocalData : function (newData) {
    var localData = this.localData,
        localDataIsEmpty = (localData == null || localData.isEmpty()),
        localDataLength = (localDataIsEmpty ? 0 : localData.length),
        prevLength = (this.lengthIsKnown() ? this.getLength() : localDataLength),
        newDataIsEmpty = (newData == null || newData.isEmpty()),
        newDataLength = (newDataIsEmpty ? 0 : newData.length),
        prevLengthIsKnown = this.lengthIsKnown(),


        notify = !(localDataIsEmpty && newDataIsEmpty),
        add = (notify && localDataIsEmpty && !newDataIsEmpty),
        remove = (notify && !localDataIsEmpty && newDataIsEmpty),
        splice = (notify && !add && !remove);

    // Calculate what the new length of the cache will be.
    this.localData = newData;
    var newLength = (this.lengthIsKnown() ? this.getLength() : newDataLength);
    this.localData = localData;

    var prevNullLength = prevLength - localDataLength,
        newNullLength = newLength - newDataLength;


    var notify = !(localDataLength + newDataLength == 0 && prevNullLength == newNullLength),
        add = notify && prevLength == 0,
        remove = notify && !add && newDataLength == 0 && prevNullLength >= newNullLength,
        splice = notify && !add && !remove;

    // Fire the pre-change notification.
    var removedRecords, removedLength, addedRecords, addedLength;
    if (add && (this._dataAdd != null || this._dataAdded != null)) {
        if (newDataLength == 0) {
            addedRecords = null;
        } else if (newNullLength == 0) {
            addedRecords = newData;
        } else {
            addedRecords = newData.duplicate();
            addedRecords.length = newLength;
            for (var i = newDataLength; i < newLength; ++i) {
                addedRecords[i] = null;
            }
        }

        if (this._dataAdd != null) {
            isc.Func.replaceWithMethod(
                this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
            this._dataAdd(addedRecords, newLength - prevLength, 0);
        }
    } else if (remove && (this._dataRemove != null || this._dataRemoved != null)) {
        removedLength = localDataLength + prevNullLength - newNullLength;
        if (localDataLength == 0) {
            removedRecords = null;
        } else {
            removedRecords = localData;
            for (var i = localDataLength; i < removedLength; ++i) {
                removedRecords[i] = null;
            }
        }

        if (this._dataRemove != null) {
            isc.Func.replaceWithMethod(
                this, "_dataRemove", isc.ResultSet.getArgString("_dataRemove"));
            this._dataRemove(removedRecords, removedLength, 0);
        }
    } else if (splice && (this._dataSplice != null || this._dataSpliced != null)) {
        removedLength = localDataLength + Math.max(0, prevNullLength - newNullLength);
        if (localDataLength == 0) {
            removedRecords = null;
        } else if (prevNullLength == 0) {
            removedRecords = localData;
        } else {
            removedRecords = localData.duplicate();
            removedRecords.length = removedLength;
            for (var i = localDataLength; i < removedLength; ++i) {
                removedRecords[i] = null;
            }
        }

        addedLength = newDataLength + Math.max(0, newNullLength - prevNullLength);
        if (newDataLength == 0) {
            addedRecords = null;
        } else if (newNullLength == 0) {
            addedRecords = newData;
        } else {
            addedRecords = newData.duplicate();
            addedRecords.length = addedLength;
            for (var i = newDataLength; i < addedLength; ++i) {
                addedRecords[i] = null;
            }
        }

        if (this._dataSplice != null) {
            isc.Func.replaceWithMethod(
                this, "_dataSplice", isc.ResultSet.getArgString("_dataSplice"));
            this._dataSplice(removedRecords, removedLength, 0, addedRecords, addedLength);
        }
    }

    // Actually change the data.
    this.localData = newData;

    // Fire the post-change notifications.
    if (add && this._dataAdded != null) {
        isc.Func.replaceWithMethod(
            this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
        this._dataAdded(addedRecords, newLength - prevLength, 0);
    } else if (remove && this._dataRemoved != null) {
        isc.Func.replaceWithMethod(
            this, "_dataRemoved", isc.ResultSet.getArgString("_dataRemoved"));
        this._dataRemoved(removedRecords, removedLength, 0);
    } else if (splice && this._dataSpliced != null) {
        isc.Func.replaceWithMethod(
            this, "_dataSpliced", isc.ResultSet.getArgString("_dataSpliced"));
        this._dataSpliced(removedRecords, removedLength, 0, addedRecords, addedLength);
    }
    this._checkLengthIsKnown(prevLengthIsKnown);
},

_checkLengthIsKnown : function (prevLengthIsKnown) {
    if (this._dataLengthIsKnownChanged != null) {
        var newLengthIsKnown = this.lengthIsKnown();
        if (prevLengthIsKnown != newLengthIsKnown) {
            isc.Func.replaceWithMethod(
                this, "_dataLengthIsKnownChanged", isc.ResultSet.getArgString("_dataLengthIsKnownChanged"));
            this._dataLengthIsKnownChanged(prevLengthIsKnown, newLengthIsKnown);
        }
    }
},


_sortLocalDataByProperties : function (properties, directions, normalizers, contexts) {



    var comparators = undefined;
    if (isc.Browser.isFirefox && isc.isA.ResultTree(this._tree) && this._tree.isPaged()) {
        var openNormalizer = this._tree._openNormalizer,
            ascendingComparator = this._tree._openAscendingComparator,
            descendingComparator = this._tree._openDescendingComparator;

        for (var i = normalizers.length; i--; ) {
            if (normalizers[i] === openNormalizer) {
                if (comparators == null) {
                    comparators = new Array(normalizers.length);
                }
                comparators[i] = (directions[i] ? ascendingComparator : descendingComparator);
            }
        }
    }


    var notifyMoved = (this._dataMoved != null),
        sortIndex = this.localData.sortByProperties(
            properties, directions, normalizers, contexts, comparators, notifyMoved, false);

    // Since the localData was just sorted and entries were possibly rearranged, reorder allRows
    // using localData if shouldReorderAllRows is set.
    if (this.shouldReorderAllRows && this.allRows != null) this.reorderAllRows();

    if (notifyMoved) {
        isc.Func.replaceWithMethod(
            this, "_dataMoved", isc.ResultSet.getArgString("_dataMoved"));



        if (sortIndex != null) {


            var localData = this.localData;

            for (var i = 0, len = localData.length; i < len; ) {
                var k = sortIndex[i];

                if (i < k) {
                    var j = i + 1,
                        l = k;
                    while (j < len && sortIndex[j] == 1 + l) {
                        ++j;
                        ++l;
                    }

                    var n = j - i,
                        records = (n > 1 ? localData.getRange(i, j) : localData[i]);
                    this._dataMoved(records, n, k, i);


                    var leftShift = i - k,
                        rightShift = n;
                    for (var m = i + n; m < len; ++m) {
                        var p = sortIndex[m];
                        if (i <= p && p < k) {
                            sortIndex[m] += n;
                        }
                    }

                    i += n;
                } else {
                    ++i;
                }
            }

        }
    }
},


// Paged Mode
// --------------------------------------------------------------------------------------------

// search backward until the first null row is found, and return the row just after it
findFirstCachedRow : function (pos) {
    for (var i = pos; i >= 0; i--) {
        if (this.localData[i] == null) return i+1;
    }
    return 0;
},

// search forward until the first null row is found, and return the row just before it
findLastCachedRow : function (pos) {
    for (var i = pos; i < this.totalRows; i++) {
        if (this.localData[i] == null) return i-1;
    }
    return this.totalRows-1;
},

// It turns out that getRangePaged() can be reentrant.  The call stack is as follows (in an
// LG): fetchRemoteData() -> shows prompt w/clickMask -> clickMask makes a synthetic mouseOut
// call on the GridRenderer of the LG -> GR mouseOut calls this.getCellRecord() which is
// plumbed through to the LG - and that method calls data.get() which leads back to here.
//
// In the reentrant case, we don't want to make a redundant call to the server (which could
// lead to an endless code loop via the reentry case).  So we handle reentry here by simply
// returning the requested rows as a slice of local cache filled with loading markers - exactly
// what getRangePaged() would have returned.
//
_getRangePaged : function (start, end, ignoreCache, fetchNow) {
    if (this._getRangePagedReentrant) {
        // honor ignoreCache
        var cache = (this.ignoreCache ? [] : this.localData) || [];
        return this.fillRangeLoading(cache.slice(start, end), end-start);
    }
    this._getRangePagedReentrant = true;
    var result = this.getRangePaged(start, end, ignoreCache, fetchNow);
    delete this._getRangePagedReentrant;
    return result;
},

// NOTE: caller can ask for as many rows as he wants, and we will fetch them all.  It is up to
// the caller to only ask for the rows that are actually really needed
getRangePaged : function (start, end, ignoreCache, fetchNow) {
    if (start < 0 || end < 0) {
        //>DEBUG
        this.logWarn("getRange(" + start + ", " + end +
                     "): negative indices not supported, clamping start to 0");
        //<DEBUG
        if (start < 0) start = 0;
    }
    if (end <= start) {
        //>DEBUG
        this.logDebug("getRange(" + start + ", " + end + "): returning empty list");
        //<DEBUG
        return [];
    }

    // if the length is known, ignore or clamp requests for rows beyond the end of the
    // dataset.  Otherwise carelessly issued requests beyond the end of the dataset would cause
    // useless fetches.  Note this implies that if you know that the cache is incomplete
    // because new rows have been inserted, you need to call invalidateCache() to get them.
    var lengthIsKnown = this.lengthIsKnown();
    if (!ignoreCache && lengthIsKnown) {
        var length = this.getLength();
        if (start > length-1 && start != 0) {
            // start is beyond last valid index
            //>DEBUG
            this.logWarn("getRange(" + start + ", " + end +
                         "): start beyond end of rows, returning empty list");
            //<DEBUG
            return [];
        } else if (end > length) {
            // end (which is non-inclusive) is beyond last valid index
            end = length;
        }
    }

    if (this.localData == null) {
        this._setLocalData([]);
    }

    // if the "ignoreCache" flag is set, ignore the cache temporarily.  If a caller is aware that
    // that the cache may soon be invalidated, they can use "ignoreCache" to trigger fetches.
    if (ignoreCache) {
        this.realCache = this.localData;
        this._setLocalData([]);
    }

    var localData = this.localData;

    // store the last getRange(), so on cache invalidation we have an idea of the most recently
    // used rows
    this.lastRangeStart = start;
    this.lastRangeEnd = end;

    // determine range we don't have (i.e. remember the first and last missing row within the range)
    var firstNonLoadingRow = this._firstLoadingRow(start, end, false),
        missingRows = false,
        firstMissingRow, lastMissingRow;

    if (firstNonLoadingRow == -1) {
        // All of the records in the range from `start` to `end` are already loading.
        missingRows = false;
    } else {
        // Skip the leading and trailing ranges of loading records.
        var startPrime = firstNonLoadingRow,
            endPrime = 1 + this._lastLoadingRow(start, end, false);


        for (var i = startPrime; i < endPrime; ++i) {
            if (localData[i] == null) {
                firstMissingRow = i;
                break;
            }
        }
        missingRows = (firstMissingRow != null);
        if (missingRows) {
            for (var i = endPrime - 1; i >= firstMissingRow; --i) {
                if (localData[i] == null) {
                    lastMissingRow = i;
                    break;
                }
            }
        }
    }


    // we're done if we have all rows locally
    if (!missingRows) {
        //>DEBUG
        this.logDebug("getRange(" + start + ", " + end + ") satisfied from cache");
        //<DEBUG
        return this.fillRangeLoading(localData.slice(start, end), end - start);
    }

    // we were missing rows; fetch the rows from the server

    var startRow, endRow;
    if (this.fetchAhead) {
        var fetchAheadRange = this._addFetchAhead(start, end, firstMissingRow, lastMissingRow,
                                                  ignoreCache);
        startRow = fetchAheadRange[0];
        endRow = fetchAheadRange[1];
    } else { // requested only
        // take the requested range literally, only avoiding refetching of cached rows
        startRow = firstMissingRow;
        endRow = lastMissingRow+1;
    }

    if (this.alwaysRequestVisibleRows) {
        startRow = start;
        endRow = end;
    }

    // store current start/endRow
    this.fetchStartRow = startRow;
    this.fetchEndRow = endRow;

    // Depending on whether fetchDelay is set, getRange() may or may not immediately
    // initiate a fetch.  The data returned to the caller always includes LOADING markers for
    // rows that aren't loaded.  When a fetch is actually initiated, we also write LOADING
    // markers into our cache to prevent duplicate fetches for the same rows.
    //
    // If we do not immediately execute a fetch, we don't immediately write LOADING markers
    // into our cache because subsequent getRange() calls may shift the range of rows that will
    // ultimately be requested, and given concurrent outstanding requests, it wouldn't be clear
    // when to clean up the LOADING markers.
    var returnData;
    if (fetchNow || this.fetchDelay == 0) {
        // fetch rows immediately
        this._fetchRemoteData();
        returnData = this.fillRangeLoading(localData.slice(start, end), end - start);
    } else {
        // set a timer to fetch rows.  When this fetch ultimately fires it will be based on the
        // most recently calculated fetch range (fetchStartRow/fetchEndRow)
        // WR - XXX - Debugging cache coherence timing issues
        //isc.logWarn("========================================================= Scheduling fetchRemote on RS " + this.ID + " in " + this.fetchDelay + "ms" +
        //                    ". Fetch range is " + this.fetchStartRow + " to " + this.fetchEndRow);
        //                    try { blah.blah += 1; } catch(e) { isc.logWarn(e.stack) }

        this.fireOnPause("fetchRemoteData", "_fetchRemoteData", this.fetchDelay);
        // return a slice of the cache, with loading markers added for missing rows
        returnData = this.fillRangeLoading(localData.slice(start, end), end - start);
    }

    // restore the real cache
    if (ignoreCache) {
        this._setLocalData(this.realCache);
        this.realCache = null;
    }
    return returnData;
},

// extend the requested range of rows into one full "resultSize" batch
_addFetchAhead : function (requestedStart, requestedEnd, firstMissingRow, lastMissingRow,
                           ignoreCache)
{
    var localData = ignoreCache ? [] : this.localData,
        length = ignoreCache ? Number.MAX_VALUE : this.getLength(),
        // round out the missing range to one full resultSize
        missingCacheSize = lastMissingRow - firstMissingRow,
        extension = Math.floor((this.resultSize - missingCacheSize)/2),
        cacheCheckStart = Math.max(0, firstMissingRow - extension),
        cacheCheckEnd = Math.min(length, lastMissingRow + extension);

    var loadingRanges = this._loadingRanges,
        numLoadingRanges = loadingRanges.length,
        k = this._getLoadingRangesIndex(cacheCheckStart),
        isLoading = (k % 2 == 0),
        j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);

    // and look for the first and last missing row within the extended range
    for (var i = cacheCheckStart; i <= firstMissingRow; i++) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        }
        if (localData[i] == null || isLoading) {
            break;
        }
    }
    firstMissingRow = i;

    k = this._getLoadingRangesIndex(cacheCheckEnd);
    isLoading = (k % 2 == 0);
    j = (k > 0 ? loadingRanges[--k] : -1);
    for (var i = cacheCheckEnd; i >= lastMissingRow; i--) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k > 0 ? loadingRanges[--k] : -1);
        }
        if (localData[i] == null || isLoading) {
            break;
        }
    }
    lastMissingRow = i;

    //>DEBUG
    this.logDebug("getRange(" + [requestedStart,requestedEnd] +
                 "), cache check: " + [cacheCheckStart, cacheCheckEnd] +
                 " firstMissingRow: " + firstMissingRow +
                 " lastMissingRow: " + lastMissingRow); //<DEBUG

    // NOTE throughout:
    // - we don't want to clamp endRow to totalRows because more rows could be
    //   have been added at the server.
    // - make sure we still fetch everything in the requested ranged (when resultSize is
    //   smaller than requested range)
    var startRow, endRow;
    if (firstMissingRow == 0 ||
        (firstMissingRow > cacheCheckStart && lastMissingRow == cacheCheckEnd))
    {
        // we have cache at the beginning of the requested range, but none at the end, so
        // we appear to be scrolling forward: fetch a slice stretching forward from the
        // first missing row
        //>DEBUG
        this.logDebug("getRange: guessing forward scrolling");
        //<DEBUG
        startRow = firstMissingRow;
        endRow = startRow + this.resultSize;

        if (endRow < requestedEnd) endRow = requestedEnd;
    } else if (firstMissingRow == cacheCheckStart && lastMissingRow < cacheCheckEnd) {
        // we have cache at the end of the requested range, but none at the beginning, so
        // we appear to be scrolling backward: fetch a slice stretching backward from the
        // last missing row
        //>DEBUG
        this.logDebug("getRange: guessing backward scrolling");
        //<DEBUG
        endRow = lastMissingRow+1;
        startRow = endRow - this.resultSize;
        if (startRow < 0) startRow = 0;

        if (startRow > requestedStart) startRow = requestedStart;
    } else {
        // we have no cache in the requested range.  Fetch a slice centered on the middle
        // of the requested range.
        //>DEBUG
        this.logDebug("getRange: no scrolling direction detected");
        //<DEBUG
        startRow = cacheCheckStart;
        endRow = cacheCheckEnd;

        if (startRow > requestedStart) startRow = requestedStart;
        if (endRow < requestedEnd) endRow = requestedEnd;
    }

    // our range to fetch may now stretch outside of the examined part of the cache.
    // Examine the fetchAhead area now to avoid fetching some already cached rows.  This is
    // key, because over-fetching is likely to happen when totalRows is about 2-5x resultSize,
    // or in general when a user is thumb-dragging in a region about 2-5x resultSize.
    k = this._getLoadingRangesIndex(startRow);
    isLoading = (k % 2 == 0);
    j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
    for (var i = startRow; i < cacheCheckStart; i++) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k + 1 < numLoadingRanges ? loadingRanges[++k] : -1);
        }
        if (localData[i] == null || isLoading) break;
    }
    //if (startRow != i) this.logWarn("trimming startRow from: " + startRow + " to " + i);
    startRow = i;

    k = this._getLoadingRangesIndex(endRow - 1);
    isLoading = (k % 2 == 0);
    j = (k > 0 ? loadingRanges[--k] : -1);
    for (var i = endRow-1; i > cacheCheckEnd; i--) {
        if (i == j) {
            isLoading = !isLoading;
            j = (k > 0 ? loadingRanges[--k] : -1);
        }
        if (localData[i] == null || isLoading) break;
    }
    //if (endRow != i+1) this.logWarn("trimming endRow from: " + endRow + " to " + (i+1));
    endRow = i+1;

    //>DEBUG
    this.logInfo("getRange(" + requestedStart + ", " + requestedEnd + ") will fetch from " +
                 startRow + " to " + endRow);
    //<DEBUG

    return [startRow, endRow];
},


// Local Mode
// --------------------------------------------------------------------------------------------

//> @method resultSet.filterLocalData() [A]
// Derive the current filtered set of data from the cache of all matching rows.
// <P>
// This method is automatically called by +link{setCriteria()} when criteria have actually
// changed, as well as in various other situations.  You could only need to call this method
// directly if:
// <ul>
// <li> you know that client-side filtering is enabled (+link{useClientFiltering}:true) and
// active +link{allMatchingRowsCached()}.
// <li> you have directly, programmatically modified data within the ResultSet such that it no
// longer matches the filter criteria
// <li> you want your modified records to disappear from the list of visible records (that is,
// those accessible via +link{get()})
// </ul>
//
// @visibility external
//<
filterLocalData : function () {

    // This method may be called from a ds-data change / cache update - in this case
    // we don't need to call dataChanged() here - it'll get called by the
    // calling code.
    var fireDataChanged = !this._isChangingData()
    if (fireDataChanged) this._startChangingData();

    this._setLocalData(this.applyFilter(
        this.allRows, this.criteria, isc.addProperties({
            dataSource: this
        }, this.context)));

    //>DEBUG
    if (this.localData != null && this.allRows != null) {
        this.logInfo(
            "Local filter applied: " + this.localData.length + " of " + this.allRows.length +
            " records matched filter:" + this.echoFull(this.criteria), "localFilter");
    }
    //<DEBUG

    // this.localData has changed,  apply sort if we have all the rows
    if (this.allRows != null && this.shouldUseClientSorting()) this._doSort();

    // NOTE: no need to apply sort, as filtering preserves order.

    // fire the dataArrived notification.  Technically, no new data has come from the server,
    // however the dataset as viewed through all public methods (get, getRange, etc) has
    // completely changed.
    // dataFromCache param passed in to indicate new data / changed data satisfied from
    // client-side cache

    if (!this._isDataArriving() && this.dataArrived) {
        this.dataArrived(0, this.localData.length, true);
    }

    if (fireDataChanged) this._doneChangingData(true, true);
},

//> @method resultSet.applyFilter() [A]
// The ResultSet will call applyFilter() when it needs to determine whether rows match the
// current filter criteria.
// <P>
// Default behavior is to call +link{dataSource.applyFilter()} to determine which rows match
// that provided criteria.
// <P>
// Override this method or +link{dataSource.applyFilter()} to implement your own client-side
// filtering behavior.
//
// @param   data        (Array)     the list of rows
// @param   criteria    (Criteria)  the filter criteria
// @param   [requestProperties]     (DSRequest Properties)  dataSource request properties
// @return  (Array)     the list of matching rows
// @visibility external
//<
applyFilter : function (data, criteria, requestProperties) {
    return this.getDataSource().applyFilter(data, criteria, requestProperties);
},

// get a list of all values for a given property.
// ValuesLists are generally used for "Select Other.." widgets where there's a field that is a
// pseudo-enum: it can have any value, but it IS an enumerated list of eg categories, and we would
// like to avoid having the same category entered twice with a slightly different name.  So we look
// for existing values among all records.
getValuesList : function (property) {
    //>DEBUG
    this.logInfo("asked for valuesList for property '" + property + "'");
    //<DEBUG

    if (this.isLocal()) {
        if (!this.allRows) {
            //>DEBUG
            this.logWarn("asked for valuesList before data has been loaded");
            //<DEBUG
            return [];
        }

        var valuesList = this.allRows.getProperty(property);
        if (!valuesList) return [];
        // NOTE: this can contain exactly one null, which we presume is an interesting value.
        // If you don't want the null, call valuesList.remove(null);
        return valuesList.getUniqueItems();
    }

    // return all unique values that happen to be cached
    var range = this.getCachedRange(),
        values = [];
    for (var i = range[0]; i <= range[1]; i++) {
        var row = this.get(i);
        if (!values.contains(row[property])) values[values.length] = row[property];
    }
    return values;
},

// Cache Management
// --------------------------------------------------------------------------------------------
// Externally callable only by very advanced callers.  Legitimate use cases include:
// - seeding a paged RS' cache with values loaded separately
// - manual cache invalidation for data dependencies not handled automaticallly
// - implementing your own comm layer

//> @method resultSet.fillCacheData() [A]
// Cache rows in this ResultSet's cache at specific positions.
// <P>
// This is used internally by ResultSet, but can also be used to seed a new ResultSet with
// already loaded data.
//
//        @param  newData      (Array of Object)   data to cache
//        @param    [startRow]   (Number)             optional start index of where new rows should be
//                                               placed in the cache; default 0
// @visibility customResultSet
//<
fillCacheData : function (newData, startRow) {
    var notifyLength = (this._dataLengthIsKnownChanged != null),
        prevLengthIsKnown = notifyLength && this.lengthIsKnown();

    if (startRow == null) startRow = 0;

    this.logDebug("integrating " + newData.length +
                  " rows into cache at position " + startRow);

    var localDataWasNull = (this.localData == null);
    if (localDataWasNull) {
        this._setLocalData([]);
    }
    var localLength = this.localData.length,
        totalLength = this._getCachedLength(),
        newDataLength = newData.length;


    this._setRangeLoading(startRow, startRow + newDataLength, false);

    if (newDataLength == 0) {

        startRow = 0;
    }
    var newTotalLength = Math.max(totalLength, startRow + newDataLength),


        headNullLength = Math.max(0, startRow - Math.min(localLength, totalLength)),
        tailNullLength = Math.max(0, newTotalLength - Math.max(localLength, startRow + newDataLength)),
        nullLength = headNullLength + tailNullLength,

        add = (newDataLength > 0 && localLength <= totalLength && localLength <= startRow),
        splice = (newDataLength > 0 && !add);


    if (splice && nullLength == 0) {
        if (this._dataSplice != null) {
            isc.Func.replaceWithMethod(
                this, "_dataSplice", isc.ResultSet.getArgString("_dataSplice"));
        }
        if (this._dataSpliced != null) {
            isc.Func.replaceWithMethod(
                this, "_dataSpliced", isc.ResultSet.getArgString("_dataSpliced"));
        }
    } else {
        if (this._dataAdd != null) {
            isc.Func.replaceWithMethod(
                this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
        }
        if (this._dataAdded != null) {
            isc.Func.replaceWithMethod(
                this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
        }
    }



    if (nullLength > 0) {

        var tempLength = Math.min(localLength, totalLength) + nullLength,
            originalRecords = (
                splice &&
                (this._dataSplice != null || this._dataSpliced != null) &&
                this.localData.slice(totalLength, localLength));

        if (add && this._dataAdd != null) {
            this._dataAdd(null, nullLength, localLength);
        } else if (splice && this._dataSplice != null) {
            this._dataSplice(originalRecords, localLength - totalLength, totalLength, null, nullLength);
        }

        if (localDataWasNull || localLength < tempLength) {
            this.localData.length = tempLength;
        } else if (localLength != tempLength) {
            this.localData = this.localData.slice(0, totalLength);
            this.localData.length = tempLength;
        }

        if (add && this._dataAdded != null) {
            this._dataAdded(null, nullLength, localLength);
        } else if (splice && this._dataSplice != null) {
            this._dataSpliced(originalRecords, localLength - totalLength, totalLength, null, nullLength);
        }

        localDataWasNull = false;
        add = add || splice;
        splice = false;
        localLength = tempLength;
        totalLength += nullLength;
    }

    var removedStartRow = Math.min(startRow, totalLength),
        removedLength = Math.min(newDataLength, Math.max(0, localLength - removedStartRow)),
        originalRecords = (
            splice &&
            (this._dataSplice != null || this._dataSpliced != null) &&
            this.localData.slice(removedStartRow, removedStartRow + removedLength));
    if (add && this._dataAdd != null) {
        this._dataAdd(newData, newDataLength, startRow);
    } else if (splice && this._dataSplice != null) {
        this._dataSplice(originalRecords, removedLength, removedStartRow, newData, newDataLength);
    }


    if (localDataWasNull || localLength < totalLength) {
        this.localData.length = newTotalLength;
    } else if (localLength != totalLength) {
        this.localData = this.localData.slice(0, totalLength);
        this.localData.length = newTotalLength;
    }

    for (var i = 0; i < newDataLength; i++) {
        var rowIndex = startRow + i,
            existingRecord = this.localData[rowIndex],
            newRecord = newData[i];

        // Increment this.cachedRows if we cached another row (as opposed to overwriting an
        // existing cached row with fresh data).
        if (newRecord != null) {
            ++this.cachedRows;
        }
        if (existingRecord != null && !Array.isLoading(existingRecord)) {
            --this.cachedRows;
        }

        this.localData[rowIndex] = newRecord;
    }

    if (add && this._dataAdded != null) {
        this._dataAdded(newData, newDataLength, startRow);
    } else if (splice && this._dataSpliced != null) {
        this._dataSpliced(originalRecords, removedLength, removedStartRow, newData, newDataLength);
    }

    if (notifyLength) {
        var newLengthIsKnown = this.lengthIsKnown();
        if (prevLengthIsKnown != newLengthIsKnown) {
            isc.Func.replaceWithMethod(
                this, "_dataLengthIsKnownChanged", isc.ResultSet.getArgString("_dataLengthIsKnownChanged"));
            this._dataLengthIsKnownChanged(prevLengthIsKnown, newLengthIsKnown);
        }
    }

    if (this.allRowsCached()) {
        this._setAllRows(this.localData, this.criteria);
    }
},

//> @method ResultSet.setFullLength() [A]
// Set the total number of rows available from the server that match the current filter
// criteria for this ResultSet.
// @param length (number) total rows available from the server
// @visibility customResultSet
//<
// NOTE: this is separate from setLength, because ResultSet.setLength() should cause deletion
// of all rows past the length.

setFullLength : function (length) {
    if (!isc.isA.Number(length)) return;
    this.logDebug("full length set to: " + length);

    // Clear out any loading markers at indices greater than or equal to `length`.
    var loadingRanges = this._loadingRanges;
    if (loadingRanges.length > 0) {
        this._setRangeLoading(length, loadingRanges.last(), false);
    }

    var isPaged = this.isPaged(),
        localLength = (this.localData && this.localData.length) || 0,
        prevLengthIsKnown = this.lengthIsKnown(),
        totalLength = (prevLengthIsKnown ? Math.max(localLength, this.getLength()) : localLength);



    if (length < totalLength) {
        var notify = (this._dataRemove != null || this._dataRemoved != null),
            removedRecords;
        if (notify && length < localLength) {
            removedRecords = this.localData.slice(length, localLength);
            for (var i = localLength; i < totalLength; ++i) {
                removedRecords[i] = null;
            }
        } else {
            removedRecords = null;
        }

        if (this._dataRemove != null) {
            isc.Func.replaceWithMethod(
                this, "_dataRemove", isc.ResultSet.getArgString("_dataRemove"));
            this._dataRemove(removedRecords, totalLength - length, length);
        }

        if (isPaged) {
            this.totalRows = length;
        }
        if (this.localData) {
            this.localData = this.localData.slice(0, length);
        }
        if (this._dataRemoved != null) {
            isc.Func.replaceWithMethod(
                this, "_dataRemoved", isc.ResultSet.getArgString("_dataRemoved"));
            this._dataRemoved(removedRecords, totalLength - length, length);
        }
    } else {
        var notify = length > totalLength;
        if (notify && this._dataAdd != null) {
            isc.Func.replaceWithMethod(
                this, "_dataAdd", isc.ResultSet.getArgString("_dataAdd"));
            this._dataAdd(null, length - totalLength, totalLength);
        }

        if (isPaged) {
            this.totalRows = length;
        }
        if (this.localData) {
            this.localData.length = length;
        }
        if (notify && this._dataAdded != null) {
            isc.Func.replaceWithMethod(
                this, "_dataAdded", isc.ResultSet.getArgString("_dataAdded"));
            this._dataAdded(null, length - totalLength, totalLength);
        }
    }
    if (this.cachedRows > length) {
        this.cachedRows = length;
    }

    if (this._dataLengthIsKnownChanged != null) {
        var newLengthIsKnown = this.lengthIsKnown();
        if (prevLengthIsKnown != newLengthIsKnown) {
            isc.Func.replaceWithMethod(
                this, "_dataLenthIsKnownChanged", isc.ResultSet.getArgString("_dataLengthIsKnownChanged"));
            this._dataLengthIsKnownChanged(prevLengthIsKnown, newLengthIsKnown);
        }
    }
},

//> @method resultSet.invalidateCache() [A]
// Manually invalidate this ResultSet's cache.
// <P>
// Generally a ResultSet will observe and incorporate updates to the DataSource that provides its
// records, but when this is not possible, <code>invalidateCache()</code> allows manual cache
// invalidation.
// <P>
// <code>invalidateCache()</code> fires <code>dataChanged()</code>, which may cause components
// using this ResultSet to request new data for display, triggering server fetches.
// @visibility external
//<
invalidateCache : function () {
    this._invalidatedRequestIndex = this._fetchingRequest;

    if (this.neverDropCache) {
        isc.logWarn("invalidateCache() called on ResultSet populated with local data (not from dataSource). Ignoring.");
        return;
    }

    // If we're updating the cache from a server operation, avoid passing the updated
    // record info to dataChanged()
    delete this._lastUpdateOperation;
    delete this._lastUpdateRecord;
    delete this._lastUpdateData;
    delete this._lastOrigRecord;
    delete this._lastUpdateRow;

    this._invalidateCache();
    // dataChanged() is required to force a refresh from the server
    if (!this._isChangingData()) this.dataChanged(null,null,null,null,true);
},

// NOTE: does not call dataChanged() automatically and should not be externally observed
_invalidateCache : function () {
    this.invalidateRows();
    this.totalRows = null; // start reporting unknown length again for paged resultSets

    //>DEBUG
    this.logInfo("Invalidating cache");
    //<DEBUG
},

// invalidateCache vs invalidateRows:
// - invalidateCache means we have no idea what the total rows are, eg the search criteria have
//   changed.  In this case it's appropriate to advertise !lengthIsKnown() and observing
//   widgets may choose to show something indicating transition to a new dataset, such as a
//   "fetching new data" message
// - invalidateRows means the row order is changing or is stale, but we are looking at
//   basically the same dataset in a new order.  lengthIsKnown() is true, and observing widgets
//   may choose to retain aspects of the view such as scroll position
invalidateRows : function () {
    var localData = this.localData,
        localLength = (localData && localData.length) || 0,
        lengthIsKnown = this.lengthIsKnown(),
        totalLength = Math.max(localLength, (lengthIsKnown ? this.getLength() : localLength));

    this.localData = null;
    var newTotalLength = (this.lengthIsKnown() ? this.getLength() : 0);
    this.localData = localData;


    var remove = (totalLength > 0 && newTotalLength == 0),
        splice = (localLength > 0 && newTotalLength > 0),
        removedRecords;

    if (remove && (this._dataRemove != null || this._dataRemoved != null)) {
        if (localLength > 0) {
            removedRecords = localData;
            for (var i = localLength; i < totalLength; ++i) {
                localData[i] = null;
            }
        } else {
            removedRecords = null;
        }

        if (this._dataRemove != null) {
            isc.Func.replaceWithMethod(
                this, "_dataRemove", isc.ResultSet.getArgString("_dataRemove"));
            this._dataRemove(removedRecords, totalLength, 0);
        }
    } else if (splice && this._dataSplice != null) {
        isc.Func.replaceWithMethod(
            this, "_dataSplice", isc.ResultSet.getArgString("_dataSplice"));
        this._dataSplice(localData, localLength, 0, null, localLength);
    }
    this.localData = this.allRows = null;
    if (remove && this._dataRemoved != null) {
        isc.Func.replaceWithMethod(
            this, "_dataRemoved", isc.ResultSet.getArgString("_dataRemoved"));
        this._dataRemoved(removedRecords, totalLength, 0);
    } else if (splice && this._dataSpliced != null) {
        isc.Func.replaceWithMethod(
            this, "_dataSpliced", isc.ResultSet.getArgString("_dataSpliced"));
        this._dataSpliced(localData, localLength, 0, null, localLength);
    }


    if (this._dataLengthIsKnownChanged != null && lengthIsKnown) {
        isc.Func.replaceWithMethod(
            this, "_dataLengthIsKnownChanged", isc.ResultSet.getArgString("_dataLengthIsKnownChanged"));
        this._dataLengthIsKnownChanged(true, false);
    }
    this.allRowsCriteria = null;
    delete this._emptyAllRowsCriteria;

    this.cachedRows = 0;
    // one time flag for invalidating rows on fetch can now be cleared
    this._invalidRowOrder = null;

    // Clear out all ranges of loading records now that the cache is invalidated.
    this._loadingRanges = [];
},

invalidateRowOrder : function () {
    this._invalidRowOrder = true;
},

rowOrderInvalid : function () {
    return this._invalidRowOrder;
},

dataChanged : function () {

    if (this.onDataChanged) this.onDataChanged()
},

dataArrived : function () {
},

// Selection
// -----------------------------------------------------------------------------------------

getNewSelection : function (initParams) {
    var selectionClass = this.getDataSource().selectionClass;
    if (selectionClass) return isc.ClassFactory.getClass(selectionClass).create(initParams);
    // otherwise null - use normal selection
}

});

isc.ResultSet.registerStringMethods({
    //> @method ResultSet.transformData()
    // <code>transformData()</code> provides an opportunity to modify data that has been
    // returned from the server, before it has been integrated into the client-side cache.
    // <P>
    // If data is not immediately suited for client-side use when it is returned from the
    // ultimate data store, this method allows it to be transformed on the client so that such
    // transform operations do not impact server scalability.
    // <P>
    // It is legal for <code>transformData()</code> to modify not only the records, but also
    // their number (by modifying startRow and endRow on the +link{DSResponse} object).
    // <P>
    // See also +link{dataSource.transformResponse()} for an alternative entry point which
    // applies to all DSResponses for a DataSource.
    //
    // @param   newData            (any)  data returned from the server
    // @param   dsResponse         (DSResponse) the DSResponse object returned by the
    //                             server
    // @return (Array of Objects) the modified data, ready to be cached
    // @visibility external
    //<
    transformData : "newData,dsResponse",

    //> @method ResultSet.dataArrived()
    // Notification fired when data has arrived from the server and has been successfully
    // integrated into the cache.
    // <P>
    // When <code>dataArrived()</code> fires, an immediate call to <code>getRange()</code> with
    // the <code>startRow</code> and <code>endRow</code> passed as arguments will return a List
    // with no +link{resultSet.getLoadingMarker(),loading markers}.
    //
    // @param startRow  (int)   starting index of rows that have just loaded
    // @param endRow    (int)   ending index of rows that have just loaded, non-inclusive
    // @visibility external
    //<
    // 'dataFromCache' - passed when new data was satisfied from a client-side cache of data

    dataArrived : "startRow,endRow,dataFromCache",

    //> @method resultSet.dataChanged()
    // Fires when data in the ResultSet has been changed.
    // <P>
    // For a single-row update, that is, a single row that has been updated, added or removed,
    // parameters such as <code>rowNum</code> are available for UI widgets that want to
    // incrementally update the display (for example, show a remove animation for a record being
    // removed).
    // <P>
    // In the single-row update case, the rowNum will indicate:
    // <ul>
    // <li> for a remove, the index where the row was removed
    // <li> for an add, the index where the row has been added
    // <li> for an update, the index of the updated row
    // </ul>
    // Note several cases for "update":
    // <ul>
    // <li> an "update" on a record not in the cache may introduce a new record to the cache.  In
    // this case, <code>originalRecord</code> is null and <code>rowNum</code> is the position of
    // insertion.
    // <li> an "update" may remove a record from cache. In this case, the rowNum indicates its
    // former position and this can be detected by checking
    // resultSet.get(rowNum) != originalRecord
    // </ul>
    // Note that an "update" on a sorted dataset may cause changes at two indices.  In this case
    // <code>dataChanged()</code> fires without parameters.
    // <P>
    // <code>dataChanged()</code> also fires in a number of situations in which the entire
    // dataset is affected and in this case no parameters are available.  This includes new
    // filter criteria, new sort direction, cache invalidation, new data arrival and manual calls
    // to <code>dataChanged()</code> triggered by customized subclasses of ResultSet.
    // <P>
    // In this case observing code should assume the dataset has been partly or wholly reordered,
    // and may have no records in common with the dataset as it existed before
    // <code>dataChanged()</code> fired.
    //
    // @param [operationType] (DSOperationType) type of operation that took place if a single row
    //                                          update, otherwise, null
    // @param [originalRecord] (Record) record before update took place.  Null for operationType
    //                                  "add".
    // @param [rowNum] (integer) row where the update took place
    // @param [updateData] (Record) +link{group:dataSourceOperations,cache update data} returned
    //                               by the server, or submitted values if no data was returned
    //                               and +link{updateCacheFromRequest} is set.
    // @visibility internal
    //<
    // 'filterChanged' boolean - passed when this method was called from invalidateCache
    // or from filterLocalData (applying a client side filter to a dataSet). In both cases
    // the visible data-set has changed due to a re-filter
    // 'dataFromCache' - passed when new data was satisfied from a client-side cache of data

    dataChanged : "operationType,originalRecord,rowNum,updateData,filterChanged,dataFromCache",



    _dataAdd : "records,length,rowNum", // before add
    _dataAdded : "records,length,rowNum", // after add
    _dataRemove : "records,length,rowNum", // before remove
    _dataRemoved : "records,length,rowNum", // after remove
    _dataSplice : "originalRecords,originalLength,rowNum,updatedRecords,updatedLength", // before splice
    _dataSpliced : "originalRecords,originalLength,rowNum,updatedRecords,updatedLength", // after splice
    _dataMoved : "records,length,originalRowNum,updatedRowNum", // after move
    _dataLengthIsKnownChanged : "originalValue,updatedValue" // after a change in the value of lengthIsKnown()
});

// isc._dataModelToString and isc._dataModelLogMessage are defined in Log.js
isc.ResultSet.getPrototype().toString = isc._dataModelToString;
isc.ResultSet.getPrototype().logMessage = isc._dataModelLogMessage;

//>!BackCompat 2004.7.29  fetchModes as distinct classes
isc.ClassFactory.defineClass("LocalResultSet", isc.ResultSet);
isc.LocalResultSet.addProperties({
    fetchMode : "local"
});

// WRS sets fetchMode : "paged", already the default
isc.ClassFactory.defineClass("WindowedResultSet", isc.ResultSet);
//<!BackCompat


//>    @method resultSet.findAll()
// Like +link{list.findAll()}.  Checks only loaded rows and will not trigger a fetch.
// @include list.findAll
//<

//>    @method resultSet.find()
// Like +link{list.find()}.  Checks only loaded rows and will not trigger a fetch.
// @include list.find
//<

//>    @method resultSet.findIndex()
// Like +link{list.findIndex()}.  Checks only loaded rows and will not trigger a fetch.
// @include list.findIndex
//<

//>    @method resultSet.findNextIndex()
// Like +link{list.findNextIndex()}.  Checks only loaded rows and will not trigger a fetch.
// @include list.findNextIndex
//<

//>    @method resultSet.getProperty()
// Like +link{list.getProperty()}.  Checks only loaded rows and will not trigger a fetch.
// @include list.getProperty
//<

isc.ResultSet.addMethods(
    isc.ClassFactory.makePassthroughMethods(
        ["find", "findIndex", "findNextIndex", "findAllIndices", "findAll", "getProperty"],
        "localData",
        true,
        "ResultSet.${methodName} called with no cached local data." +
        " Calling code can use ResultSet.lengthIsKnown() to determine whether the ResultSet" +
        " has loaded any data.",
        "dataSource"
    )
);



//> @groupDef treeDataBinding
//
// The SmartClient +link{TreeGrid} component is a visual representation of a tree and requires
// a +link{Tree} or +link{ResultTree} datatype passed via the +link{TreeGrid.data} attribute to
// initialize the tree view.  The +link{Tree} datatype is used when you want to provide all of
// the tree nodes in one shot at initialization time.  The +link{ResultTree} datatype is used
// when you want portions of the tree to be loaded on demand from the server.
// <p>
// <b>Providing all data to the Tree at creation</b>
// <p>
// The simplest mechanism by which to initialize a Tree is to simply provide all the data
// up-front when the Tree itself is created.  Depending on the format of your tree data, this
// can be done by setting +link{Tree.root} or +link{Tree.data}.  This functionality is provided
// by the +link{Tree} class.
// <p>
// For examples of this type of databinding, see the following SDK examples:
// <ul>
// <li>+explorerExample{childrenArrays, TreeGrid Initialization Example}</li>
// <smartclient>
// <li>+externalLink{/examples/server_integration/#jstlTree, TreeGrid Initialization with JSTL}</li>
// </smartclient>
// </ul>
// <p>
// <b>Loading Tree nodes on demand</b>
// <p>
// In this mode, tree nodes are loaded on-demand the first time a user expands a folder.  This
// approach is necessary for large trees.  This functionality is provided by the
// +link{ResultTree} class, which uses a +link{DataSource} to load data from the server.  Each
// DataSource Record becomes a +link{TreeNode}.
// <p>
// When the user expands a folder whose children have not yet been loaded
// from the server (or you programmatically call openFolder() on such a node), the client
// automatically sends a +link{DSRequest} to the server to ask for all immediate children of
// that node.
// <P>
// If you have a dataset that is +link{TreeModelType,"parent-linked"}, that is, every node has
// a unique ID (the +link{tree.idField}) and also has a property with the unique ID of it's
// parent node (the +link{tree.parentIdField}) the tree can load child nodes by simply sending
// a DSRequest with appropriate +link{Criteria}.  Given a parent node with ID "225" in a tree
// where the +link{tree.parentIdField} is called "parentId", the criteria would be:
// <pre>
//    { parentId : 225 }
// </pre>
// The client is asking the server: "give me all nodes whose parentId is 225", which are the
// children of node 225.
// <P>
// If you have a DataSource that supports simple +link{Criteria} like the above, and your
// records have nodes with ids and parentIds, this strategy can be used by just declaring the
// tree relationship in your DataSource: the tree will automatically use your
// +link{DataSourceField.primaryKey} field as the +link{Tree.idField}.  To declare the
// +link{Tree.parentIdField}, declare a +link{DataSourceField.foreignKey} field with the
// name of the primaryKey field.
// <P>
// If you have a tree where there is no convenient unique ID, for example, you have mixed types
// of nodes (for example, departments and employees), use one of the following approaches:
// <ol>
// <li> generate a synthetic node ID and return it with every tree node.
// <P>
// Typically two or more properties can be combined into a String that serves as a unique ID.
// For example, if you are loading a mixed tree of "Departments" and "Users", each of which
// have unique numeric IDs, you could generate synthetic node IDs like "department:353" and
// "user:311".  Your server-side code will then receive these synthetic node IDs when the tree
// loads children, and you can parse the IDs, look up the appropriate object and return its
// child nodes.
// <P>
// In the case of filesystems or XML documents, you can use the full path to the file or XML
// element as the unique ID.
// <P>
// <li> have all properties of the parentNode +link{DataSource.sendParentNode,sent to the server}
// <P>
// If having all the properties of the parentNode would allow you to look up children, this
// approach may be more convenient than having to generate synthetic node IDs and parse them
// when looking up children.
// <P>
// For example, with a mixed-type tree, your server-side code may be able to quickly identify
// the type of the parent node be looking for specific properties, and then call methods to
// look up children for that type of node.
// <P>
// In this case there is no need to declare an idField or parentIdField.
// </ol>
// <P>
// +link{ResultTree}s are created for you by the +link{TreeGrid} when you set
// +link{TreeGrid.dataSource}, but you can pass an initial dataset to a databound TreeGrid by
// setting +link{TreeGrid.initialData}.
// <P>
// If you do not provide +link{TreeGrid.initialData}, the first DSRequest you receive will be a
// request for the nodes under root.  The id of the root node of the tree is the value of the
// <code>rootValue</code> attribute on the +link{Tree.parentIdField} of the Tree DataSource.
// <p>
// For examples of this type of databinding, see the following SDK examples:
// <ul>
// <li>+explorerExample{initialData, TreeGrid DataBinding Example}</li>
// <smartclient>
// <li>+externalLink{/examples/server_integration/#xml2JSLOD, TreeGrid XML DataBinding}
// </smartclient>
// </ul>
// <P>
// <b>Folders and load on demand</b>
// <P>
// When using load on demand, the Tree cannot simply check whether a node has children to
// determine whether it's a folder, and will assume all loaded nodes are folders.  To avoid
// this, you can add a boolean field to your DataSource called "isFolder" that indicates
// whether a node is a folder or not.  If you already have a boolean field that indicates
// whether a node is a folder, you can instead set +link{tree.isFolderProperty} to the name of
// that field via +link{TreeGrid.dataProperties}.
// <P>
// <b>Multi-Level load on demand</b>
// <P>
// The ResultTree's DSRequests ask for the immediate children of a node only (by specifying
// <code>parentId</code> in the criteria). Any nodes returned whose <code>parentId</code> field
// value is unset or matches this criterion will be added to the tree as immediate children of the
// node. However you are also free to return multiple levels of children.  This can be done by
// simply returning a flat list of descendents with valid id's and parentId's, exactly as though
// you were initializing a multi-level tree via +link{Tree.data}.
// <P>
// Note that when receiving multiple levels of children, the ResultTree's assumption is that
// if any children are loaded for a parent, then that parent is considered fully loaded.
// <P>
// When loading children for a given parent node, the ResultTree calls
// +link{DataSource.fetchData} on its DataSource.  For custom code that may need to reference
// the parentNode or tree in some way, the parent node whose children are being loaded is
// available on the dsRequest instance in the DataSource flow as dsRequest.parentNode, where it
// can be inspected during +link{DataSource.transformRequest()}.
// <P>
// For an example of this feature, see the following SDK example:
// <ul>
// <li>+explorerExample{multiLevelLOD,Multi-Level Load on Demand Example}</li>
// </ul>
// <P>
// <b>Paging large sets of children</b>
// <p>
// If some nodes in your tree have a very large number of immediate children, you can enable
// +link{resultTree.fetchMode,fetchMode:"paged"} to load children in batches.  This means that
// whenever the children of a folder are loaded, the <code>resultTree</code> will set
// +link{dsRequest.startRow} and +link{dsRequest.endRow,endRow} when requesting children from
// the DataSource.  This includes the initial fetch of top-level nodes, which are children of
// the +link{tree.showRoot,implicit root node}.
// <p>
// As with all paged DSRequests, the server is free to ignore startRow/endRow and
// simply return all children of the node.  This allows the server to make on-the-fly
// folder-by-folder choices as to whether to use paging or just return all children.  However,
// whenever the server returns only some children, the server must provide an accurate value for
// +link{dsResponse.totalRows}.
// <p>
// If the server does return a partial list of children, the <code>resultTree</code> will
// automatically request further children as they are accessed; typically this happens because
// the user is scrolling around in a +link{TreeGrid} which is viewing the
// <code>resultTree</code>.
// <p>
// In this mode, the server may return multiple levels of the tree as described above
// ("Multi-Level load on demand"), however, by default the server is not allowed to return
// folders that are open, as this creates a potential performance issue: consider the case of a
// user scrolling rapidly into an unloaded area of the tree, skipping past many nodes that have
// not been loaded.  If the skipped nodes might have been open parents, then the only way to
// know what nodes should be visible at the new scroll position is to load all skipped nodes
// and discover how many visible children they had.
// <p>
// If this performance consequence is acceptable, the restriction against returning open
// folders from the server may be lifted on a tree-wide basis by setting the
// +link{resultTree.canReturnOpenFolders,canReturnOpenFolders} property to <code>true</code>
// and/or on a folder-by-folder basis by setting the property named by the
// +link{resultTree.canReturnOpenSubfoldersProperty,canReturnOpenSubfoldersProperty} to
// <code>true</code>.  In this case, it is recommended to also set
// +link{resultTree.progressiveLoading,progressiveLoading} to <code>true</code> to prevent
// users from causing a large number of nodes to be loaded by scrolling too far ahead in the
// tree.
// <p>
// In addition, if any folder is returned already open, it must include children via the
// +link{tree.childrenProperty,childrenProperty} or there will be an immediate, new fetch to
// retrieve the children.  When returning children, a partial list of children may be
// returned, but if so, the +link{resultTree.childCountProperty,childCountProperty} must be
// set to the total number of children.
// <p>
// Paged ResultTrees may also be filtered like other trees (see
// +link{resultTree.setCriteria}).  However, if +link{resultTree.keepParentsOnFilter} is
// enabled then server filtering is required.  To illustrate with an example, consider a case
// where the ResultTree has 10,000 folders at root level and where criteria applied to their
// children would eliminate all but 20, which happen to be at the end of the 10,000.  Purely
// client-side logic would have to perform 10,000 fetch operations to check whether each
// root-level node had children before arriving at the final set of 20.
// <p>
// For examples of this feature, see the following SDK example:
// <ul>
// <li>+explorerExample{pagingForChildren,Paging for Children Example}</li>
// </ul>
// <p>
// <b>NOTE:</b> trees with thousands of visible nodes are very difficult for end users to
// navigate.  A <b>majority of the time</b> the best interface for showing a very large tree
// is to show a TreeGrid that displays just folders, adjacent to a ListGrid that shows items
// within those folders.
// <p>
// For example, the data in your email account can be thought of as an enormous tree of
// folders (Inbox, Sent, Drafts, Trash etc) with thousands of messages in each folder.
// However, none of the common email clients display email this way; all of them choose to
// show folders and messages separately, as this is clearly more usable.
// <p>
// Before starting on implementing paging within sets of children, carefully consider whether
// an interface like the above, or some entirely different interface, is actually a superior
// option.  It is exceedingly rare that paging within sets of children is the best choice.
//
// @title Tree DataBinding
// @treeLocation Client Reference/Data Binding
// @visibility external
//<


//>    @class ResultTree
// ResultTrees are an implementation of the +link{class:Tree} API, used to handle hierarchical
// data, whose nodes are DataSource records which are retrieved from a server.
//
// @visibility external
// @treeLocation    Client Reference/Data Binding
//<
isc.ClassFactory.defineClass("ResultTree", isc.Tree);

isc.ResultTree.addClassProperties({

    getLoadingMarker : function () {
        return (isc.ResultSet != null ? isc.ResultSet.getLoadingMarker() : Array.LOADING);
    }
});

isc.ResultTree.addProperties({
    nameProperty:"__nodePath",
    nodeTypeProperty:"nodeType",
    childTypeProperty:"childType",
    modelType: "parent",

    // DataModel
    // ---------------------------------------------------------------------------------------

    //> @attr resultTree.data (List of TreeNode : null : IRA)
    // Optional initial data for the tree.  If the +link{resultTree.fetchMode,fetchMode} is
    // <code>"basic"</code> or <code>"local"</code> then the format of this data is exactly
    // the same +link{tree.parentIdField,parentId}-linked list of tree nodes as
    // documented on +link{Tree.data} (when the <code>modelType</code> is set to
    // <code>"parent"</code>).  If the <code>fetchMode</code> is <code>"paged"</code> then the
    // format is extended to allow the +link{resultTree.childCountProperty,childCountProperty}
    // to be set on folder nodes.
    // <P>
    // Providing an initial set of nodes in this way does not affect the behavior of the
    // ResultTree in its loading of unloaded folders.  An equivalent result is achieved if the
    // first fetch from the server returns this same data.
    // <P>
    // If <code>fetchMode</code> is <code>"paged"</code> then you may make folder-by-folder
    // choices as to whether to use paging for the childen of each folder.  If you would like
    // to use paging in a folder then you may include a partial list of that folder's children
    // with the data, provided that you set the <code>childCountProperty</code> to the total
    // number of children.  Otherwise you will need to include either all children of the
    // folder or none of the children.  Open folders without any children provided will cause
    // immediate, new fetches for the children, as usual.
    // <P>
    // Because the initial data is treated exactly as though it were returned from the tree's
    // first server fetch, the order of the initial data must match the initial sort order of
    // the TreeGrid displaying the data or, if no such sort is specified, the native storage
    // order on the server.  For example, consider initial data containing <code>n</code>
    // records having the <code>parentId</code> <code>"X"</code>, meaning they are all in
    // the same folder.  These <code>n</code> records are the records at indices
    // <code>0</code> through <code>(n - 1)</code> that are stored on the server under the
    // parent node.  If the <code>childCountProperty</code> set on the parent node indicates
    // that there are <code>m > n</code> total rows under the parent node then the records at
    // indices <code>n</code> to <code>(m - 1)</code> will be fetched from the server as the user
    // scrolls the additional rows into view.
    //
    // @see attr:Tree.data
    // @see TreeNode
    // @group treeDataBinding
    // @visibility external
    //<

    //> @attr resultTree.dataSource (DataSource or ID : null : IR)
    //  What +link{class:DataSource} is this resultTree associated with?
    //
    // @include dataBoundComponent.dataSource
    // @visibility external
    //<

    //> @attr resultTree.context (OperationContext : null : IRA)
    // OperationContext to be sent with all operations performed by this ResultTree.
    //<

    //> @attr resultTree.loadDataOnDemand (Boolean : true : IR)
    // Does this resultTree load data incrementally as folders within the tree are opened, or
    // is it all loaded in a single request?
    // @see treeGrid.loadDataOnDemand
    // @visibility external
    //<
    loadDataOnDemand:true,

    //> @attr resultTree.autoPreserveOpenState (PreserveOpenState : "whenUnique" : IRW)
    // Controls what happens to the +link{getOpenState(),"open state"} - the set of
    // nodes opened or closed by the end user after tree data is loaded - when an entirely
    // new tree of nodes is loaded from the server, as a consequence of calling
    // +link{invalidateCache()} or of changing criteria such that the current cache of
    // nodes is dropped.
    // @visibility external
    //<
    autoPreserveOpenState:"whenUnique",

    //> @type PreserveOpenState
    // @value never
    //  Never try to automatically preserve the openState.  Nodes will be initially open
    //  or closed based solely on the +link{tree.openProperty} optionally set by the server.
    // @value whenUnique
    //  If either the +link{tree.idField} or +link{tree.nameProperty} has been set on
    //  the Tree, (so that nodes have either unique ids or unique paths),
    //  preserve openState by respecting the +link{tree.openProperty} set by the server,
    //  then applying the openState.
    // @value always
    //  Like "whenUnique" but automatically preserves openState even if nodes cannot be
    //  uniquely identified.  This means that nodes at the same tree positions
    //  (eg 3rd child of 5th node under root) will be placed in the same openState,
    //  regardless of whether that node has anything to do with the node that previously
    //  was at that tree position.
    //
    // @visibility external
    //<

    //> @attr resultTree.fetchMode (FetchMode : "basic" : IR)
    // Mode of fetching records from server.
    // <P>
    // fetchMode:"local" implies that local filtering will always be performed. See
    // +link{keepParentsOnFilter} for additional filtering details.
    // <P>
    // fetchMode:"basic" or "paged" implies that if search criteria change, the entire
    // tree will be discarded and re-fetched from the server.  When retrieving the replacement
    // tree data, the default behavior will be to preserve the +link{getOpenState,openState}
    // for any nodes that the server returns which were previously opened by the user.  Note
    // that this implies that if +link{loadDataOnDemand} is enabled and the server returns only
    // root-level nodes, open state will be preserved only for root-level nodes, and children
    // of open root-level nodes will be immediately fetched from the server if
    // they are not included in the server's initial response.
    // <P>
    // fetchMode:"paged" enables paging for nodes that have very large numbers of children.
    // Whenever the children of a folder are loaded, the <code>resultTree</code> will set
    // +link{dsRequest.startRow} and +link{dsRequest.endRow,endRow} when requesting children
    // from the DataSource, and will manage loading of further children on demand, similar to
    // how a +link{ResultSet} manages paging for lists.  For a deeper discussion see the
    // <b>Paging large sets of children</b> section of the +link{group:treeDataBinding}
    // overview.
    //
    // @example pagingForChildren
    // @group treeDataBinding
    // @visibility external
    //<
    fetchMode:"basic",


    //> @attr resultTree.resultSize (integer : 75 : IRA)
    // How many tree nodes to retrieve at once from each large set of children in the tree.
    // <P>
    // Applicable only with <code>fetchMode: "paged"</code>.  When a paged ResultTree is asked
    // for rows that have not yet been loaded, it will fetch adjacent rows that are likely to
    // be required soon, in batches of this size.
    // @group treeDataBinding
    // @visibility external
    //<
    resultSize: 75,


    _childrenResultSetProperties: {
        fetchMode: "paged",


        _dataAdd : function (records, length, rowNum) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataAdd(this, parentNode, records, length, rowNum, true);
        },
        _dataAdded : function (records, length, rowNum) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataAdd(this, parentNode, records, length, rowNum, false);
        },
        _dataRemove : function (records, length, rowNum) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataRemove(this, parentNode, records, length, rowNum, true);
        },
        _dataRemoved : function (records, length, rowNum) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataRemove(this, parentNode, records, length, rowNum, false);
        },
        _dataSplice : function (originalRecords, originalLength, rowNum, updatedRecords, updatedLength) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataSplice(
                this, parentNode, originalRecords, originalLength, rowNum,
                updatedRecords, updatedLength, true);
        },
        _dataSpliced : function (originalRecords, originalLength, rowNum, updatedRecords, updatedLength) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataSplice(
                this, parentNode, originalRecords, originalLength, rowNum,
                updatedRecords, updatedLength, false);
        },
        _dataMoved : function (records, length, originalRowNum, updatedRowNum) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataMoved(this, parentNode, records, length, originalRowNum, updatedRowNum);
        },
        _dataLengthIsKnownChanged : function (originalValue, updatedValue) {
            var tree = this._tree,
                parentNode = this._parentNode;
            tree._childrenDataLengthIsKnownChanged(this, parentNode, originalValue, updatedValue);
        }
    },

    //> @attr resultTree.childCountProperty (String : "childCount" : IR)
    // When using +link{fetchMode,fetchMode:"paged"} and providing multiple levels of the tree in
    // one DSResponse, <code>childCountProperty</code> must be set for any folders that include
    // only a partial list of children.
    // For a deeper discussion see the <b>Paging large sets of children</b> section of the
    // +link{group:treeDataBinding} overview.
    // @example multiLevelChildPaging
    // @visibility external
    //<
    childCountProperty: "childCount",

    //> @attr resultTree.canReturnOpenSubfoldersProperty (String : "canReturnOpenSubfolders" : IR)
    // When using +link{fetchMode,fetchMode:"paged"} and providing multiple levels of the tree
    // in one DSResponse, <code>canReturnOpenSubfoldersProperty</code> may be set on any
    // folder to indicate whether child folders might be returned by the server already open.
    // If the property is set to false on a folder then subfolders of that folder are never
    // allowed to be returned already open.  This enables the paging mechanism to be more
    // efficient in the amount of data that it requests from the server.
    // <P>
    // For example, setting the <code>canReturnOpenSubfoldersProperty</code> value to
    // <code>false</code> on a node is appropriate if the server-side code determines that the
    // the node's children consist of entirely leaf nodes.
    // @see resultTree.canReturnOpenFolders
    // @visibility external
    //<
    canReturnOpenSubfoldersProperty: "canReturnOpenSubfolders",

    //> @attr resultTree.canReturnOpenFolders (boolean : false : IR)
    // When using +link{fetchMode,fetchMode:"paged"} and providing multiple levels of the tree
    // in one DSResponse, this property specifies the default value assumed for the
    // +link{canReturnOpenSubfoldersProperty} when no value for that property is provided for
    // a node.
    // @visibility external
    //<
    canReturnOpenFolders: false,

    //> @attr resultTree.progressiveLoading (boolean : null : IRW)
    // Sets +link{DataSource.progressiveLoading,progressive loading mode} for this
    // ResultTree.  The ResultTree will copy this setting onto the +link{DSRequest}s that it
    // issues, overriding the OperationBinding- and DataSource-level settings, in cases where
    // the use of progressive loading does not affect the correctness of the tree's paging
    // algorithm.
    // <p>
    // This setting is applied automatically by +link{DataBoundComponent}s that have their own
    // explicit setting for +link{DataBoundComponent.progressiveLoading,progressiveLoading}.
    // <p>
    // <b>Note:</b>  This property only has an effect for +link{fetchMode,fetchMode:"paged"}
    // ResultTrees.
    // @see dataSource.progressiveLoading
    // @see operationBinding.progressiveLoading
    // @see dsRequest.progressiveLoading
    // @see dataBoundComponent.progressiveLoading
    // @group progressiveLoading
    // @visibility external
    //<

    //> @attr resultTree.defaultIsFolder (boolean : null : IR)
    // Controls whether nodes are assumed to be folders or leaves by default.
    // <P>
    // Nodes that have children or have the +link{tree.isFolderProperty,isFolderProperty} set
    // to true will always be considered folders.  Other nodes will be considered folders or
    // leaves by default according to this setting.
    // <P>
    // If <code>defaultIsFolder</code> is unset, the ResultTree will automatically set it to
    // match the value of +link{loadDataOnDemand}.  This means that, when using
    // folder-by-folder load on demand (<code>loadDataOnDemand:true</code>), by default a newly
    // loaded node will be considered to be a folder that has not loaded its children yet.
    // <P>
    // When not using folder-by-folder load on demand, by default a newly loaded node is
    // considered a leaf.  If you set <code>defaultIsFolder:true</code> explicitly, by default
    // a newly loaded node is considered to be a folder with no children.
    // <P>
    // See +link{Tree.isFolder()} for details on how to explicitly mark nodes as folders or leaves.
    //
    // @see treeGrid.loadDataOnDemand
    // @visibility external
    //<

    //> @attr resultTree.rootNode (any : null :IR)
    // This attribute may be used to specify a root value for the parentIdField of this resultTree.
    // This overrides the default +link{DataSourceField.rootValue} for this tree, allowing
    // a component to navigate a tree starting at a specific node.
    // <P>
    // May be overridden via +link{TreeGrid.treeRootValue} for ResultTrees generated by a TreeGrid
    // component.
    // @visibility external
    //<

    //> @attr resultTree.discardParentlessNodes (boolean : null : IRA)
    // When data is loaded from the server, should nodes with an explicit value for
    // the +link{tree.parentIdField} which doesn't map to a valid parent node be dropped?
    // If set to false, for +link{TreeGrid.loadDataOnDemand}:false trees, parentless nodes will be
    // added as children of the root node - for +link{TreeGrid.loadDataOnDemand}:true, they will be
    // added as children of the folder currently requesting children.
    // <P>
    // This effectively allows nodes to be loaded into the current (or root) folder without
    // needing an explicit +link{tree.parentIdField,parentIdField value} that matches the folder's
    // ID or <code>rootValue</code> for the resultTree.
    // <P>
    // Note: For <code>loadDataOnDemand:false</code> trees, if this property is unset at init time,
    // it will default to <code>true</code> if an explicit +link{resultTree.rootNode} has been
    // specified. This ensures that if the data tree retrieved from the server includes ancestors
    // of the desired root-node we don't display them. Otherwise this property always defaults to
    // false.
    // @visibility external
    //<

    //>@attr ResultTree.defaultNewNodesToRoot (Boolean : false : IRWA)
    // This attribute governs how to handle cache-synch when a new node is added to this dataSource
    // with no explicit parentId.
    // <P>
    // If set to <code>true</code>, when a new node is added to this dataSource via
    // +link{DataSource.addData()}, with no explicit parentId, the node will be added as a
    // child of the root node of this result tree. Otherwise it will be ignored.
    // <P>
    // Similar logic applies to +link{DataSource.updateData(),updated nodes} - if this property is
    // true and the parentId of an updated node is cleared, it will be moved to become a child of
    // root, otherwise it will be dropped from the tree.
    // @visibility external
    //<
    defaultNewNodesToRoot:false,


    //> @attr resultTree.updateCacheFromRequest (Boolean : true : IRA)
    // When a successful Add, Update or Remove type operation fires on this ResultTree's
    // dataSource, if +link{dsResponse.data} is unset, should we integrate the submitted
    // data values (from the request) into our data-set?
    //
    // @group cacheSync
    // @visibility external
    //<
    updateCacheFromRequest:true

    //> @attr   resultTree.disableCacheSync (Boolean : false : IRA)
    // By default when the data of this ResultTree's dataSource is modified, the ResultTree will
    // be updated to display these changes.
    // Set this flag to true to disable this behavior.
    // @group cacheSync
    // @visibility external
    //<

    // Filtering
    // ----------------------------------------------------------------------------------------

    //> @attr resultTree.keepParentsOnFilter (boolean : null : IR)
    // If set, tree-based filtering is performed such that parent nodes are kept as long as
    // they have children that match the filter criteria, even if the parents themselves do not
    // match the filter criteria. If not set, filtering will exclude parent nodes not matching
    // the criteria and all nodes below it in the tree.
    // <P>
    // When <code>keepParentsOnFilter</code> is enabled for paged ResultTrees, server-side
    // filtering is required.
    // <P>
    // When enabled for non-paged trees, +link{fetchMode,fetchMode:"local"} is automatically
    // enabled so that all filtering behavior shifts to the client-side and full criteria are
    // no longer sent to the server.  Instead, server fetches will always load all nodes, or
    // with +link{loadDataOnDemand}:true, will always load all nodes under a given parent.
    // This means that the server does not need to implement special tree filtering logic.
    // <P>
    // Optionally, +link{resultTree.serverFilterFields} can be set to a list of field names that
    // will be sent to the server whenever they are present in the criteria.
    // @group treeDataBinding
    // @visibility external
    //<


    //> @attr resultTree.serverFilterFields (Array of String : null : IR)
    // When +link{keepParentsOnFilter} is enabled for +link{fetchMode,fetchMode:"local"}
    // ResultTrees, this property lists field names that will be sent to the server if they
    // are present in the criteria.
    // @visibility external
    //<
});

isc.ResultTree.addMethods({


add : function (node, parent, position) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation (add) will " +
            "be ignored.");
    } else {
        return this.invokeSuper(isc.ResultTree, "add", node, parent, position);
    }
},
addList : function (nodeList, parent, position) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation (addList) " +
            "will be ignored.");
    } else {
        return this.invokeSuper(isc.ResultTree, "addList", nodeList, parent, position);
    }
},
linkNodes : function (records, idProperty, parentIdProperty, rootValue, isFolderProperty, contextNode, suppressDataChanged) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation " +
            "(linkNodes) will be ignored.");
    } else {
        return this.invokeSuper(
            isc.ResultTree, "linkNodes",
            records, idProperty, parentIdProperty, rootValue, isFolderProperty, contextNode,
            suppressDataChanged);
    }
},
move : function (node, newParent, position) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation (move) " +
            "will be ignored.");
    } else {
        return this.invokeSuper(isc.ResultTree, "move", node, newParent, position);
    }
},
remove : function (node, noDataChanged) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation (remove) " +
            "will be ignored.");
    } else {
        return this.invokeSuper(isc.ResultTree, "remove", node, noDataChanged);
    }
},
removeList : function (nodeList) {
    if (this.isPaged()) {
        isc.logWarn(
            "ResultTrees with fetchMode \"paged\" are read-only.  This operation " +
            "(removeList) will be ignored.");
    } else {
        return this.invokeSuper(isc.ResultTree, "removeList", nodeList);
    }
},


//>    @method    resultTree.init()    (A)
//            Initialize this ResultTree.  Pass in objects with properties to add or override
//            defaults.
//
//        @param    [all arguments]    (object)    objects with properties to override from default
//<
init : function (a,b,c,d,e,f) {
    // create a pointer to us in the global context
    isc.ClassFactory.addGlobalID(this);

    if (!this.criteria) this.criteria = {};

    if (!this.operation) this.operation = {operationType : "fetch"};

    // dataSource can be specified either on the operation or the ResultTree.
    if (!this.dataSource) this.dataSource = this.operation.dataSource;
    if (!this.operation.dataSource) this.operation.dataSource = this.dataSource;


    if (isc.isAn.Array(this.dataSource)) {
        this.dataSource = this.dataSource[0];
        this.operation.dataSource = this.dataSource;
    }

    // If any of rootValue, idField, parentIdField are not explicitly specified on this
    // ResultTree, autodetect them from the DataSource relationship.
    if (!this.isMultiDSTree()) {

        // root node has to exist for getTreeRelationship to work, so create it now if it
        // doesn't exist
        if (!this.root) this.root = this.makeRoot();
        var relationship = this.getTreeRelationship(this.root);

        var undef;
        // compare to undef because rootValue can be set to null
        if (this.rootValue === undef) this.rootValue = relationship.rootValue;

        // If we're not loading on demand, and the rootValue is not null/undef,
        // 'discardParentlessNodes' to true.
        // This ensures that if we load an entire tree, and have a rootValue set to pick up
        // a sub-tree of that, we don't add the full tree's top level element to root and thus
        // show the entire tree
        if (!this.loadDataOnDemand &&
            (this.rootValue != null || (this.root != null && this.root[this.idField] != null)) &&
            this.discardParentlessNodes == null)
        {
            this.discardParentlessNodes = true;
        }

        if (this.idField == null) this.idField = relationship.idField;
        if (this.parentIdField == null) this.parentIdField = relationship.parentIdField;
        if (relationship.childrenProperty) this.childrenProperty = relationship.childrenProperty;

        this.root[this.idField] = this.rootValue;
    }

    // establish default values for isFolderProperty et al that were not derived from the tree
    // relationship
    this.setupProperties();

    // For paged ResultTrees, use the same resultSize for every ResultSet of children.
    if (this.isPaged()) {
        // context.dataPageSize may be set if specified on a DataBoundComponent that created us.
        var context = this.context;
        this.resultSize = (
            context && context.dataPageSize != null ? context.dataPageSize : this.resultSize);

        this._childrenResultSetProperties = isc.addProperties({}, this._childrenResultSetProperties, {
            resultSize: this.resultSize
        });
    }

    // keepParentsOnFilter usually implies fetchMode:"local".
    if (this.keepParentsOnFilter && !(this.isPaged() || this.isLocal())) {
        this.fetchMode = "local";
    }

    if (this.initialData) {
        if ("parent" == this.modelType) this.data = this.initialData;
        else if ("children" == this.modelType) this.root = this.initialData;
    }

    // observe dataChanged on our dataSource
    var dataSource = isc.DataSource.getDataSource(this.dataSource);
    this.observe(dataSource, "dataChanged", "observer.dataSourceDataChanged(dsRequest,dsResponse);");

    // whether to invalidate our cache when an update occurs on one of our datasources.
    // Default is update the current cache in place.
    this.dropCacheOnUpdate = this.operation.dropCacheOnUpdate;

    // set up defaultIsFolder before invoking Tree.init
    // This is required in _linkNodes() to ensure LOD ResultTrees' child nodes show up as
    // openable folders.
    if (this.defaultIsFolder == null) this.defaultIsFolder = this.loadDataOnDemand;

    this.invokeSuper(isc.ResultTree, "init", a,b,c,d,e,f);

    // if we're not using folder-by-folder load on demand, all nodes should be initially marked loaded
    this.setDefaultLoadState(this.loadDataOnDemand ? isc.Tree.UNLOADED : isc.Tree.LOADED);
},


setupProperties : function () {
    this.invokeSuper(isc.ResultTree, "setupProperties");

    if (this.isPaged()) {
        // An auto-generated property name to store precomputed booleans for whether a node and
        // all of its visible descendants are all loaded.
        this._visibleDescendantsCachedProperty = ("_visibleDescendantsCached_" + this.ID);


        this._initialLoadingStateProperty = ("_initialLoadingState_" + this.ID);
        this._initialLoadingFetchCountProperty = ("_initialLoadingFetchCount_" + this.ID);
    }
},


duplicate : function (includeData, includeLoadState) {
    if (!this.isPaged()) {
        return this.invokeSuper(isc.ResultTree, "duplicate", includeData, includeLoadState);
    }

    var serverFilterFields = this.serverFilterFields;
    if (isc.isAn.Array(serverFilterFields)) {
        serverFilterFields = serverFilterFields.duplicate();
    }


    var newResultTreeConfig = {
        fetchMode: this.fetchMode,
        dataSource: this.dataSource,
        loadDataOnDemand: this.loadDataOnDemand,
        childCountProperty: this.childCountProperty,
        defaultIsFolder: this.defaultIsFolder,
        discardParentlessNodes: this.discardParentlessNodes,
        defaultNewNodesToRoot: this.defaultNewNodesToRoot,
        updateCacheFromRequest: this.updateCacheFromRequest,
        disableCacheSync: this.disableCacheSync,
        keepParentsOnFilter: this.keepParentsOnFilter,
        serverFilterFields: serverFilterFields,
        canReturnOpenFolders: this.canReturnOpenFolders
    };
    this._copyKnownProperties(newResultTreeConfig);


    newResultTreeConfig.autoOpenRoot = false;

    var newResultTree = isc.ResultTree.create(newResultTreeConfig),
        root = this.getRoot(),
        rootIsOpen = this.isOpen(root),
        rootIsFolder = this.isFolder(root),
        rootCachedLength = root[this._cachedLengthProperty],
        rootRecursionCount = root[this._recursionCountProperty],
        rootAllCached = root[this._visibleDescendantsCachedProperty],
        newRoot = this.getCleanNodeData(root, false, false, includeLoadState);

    newRoot[newResultTree.openProperty] = rootIsOpen;
    newRoot[newResultTree.isFolderProperty] = rootIsFolder;
    newRoot[newResultTree._cachedLengthProperty] = rootCachedLength;
    newRoot[newResultTree._recursionCountProperty] = rootRecursionCount;
    newRoot[newResultTree._visibleDescendantsCachedProperty] = rootAllCached;

    this._duplicate(root, newResultTree, newRoot, includeLoadState);
    newResultTree.setRoot(newRoot);
    return newResultTree;
},


_getCleanNodeData : function (newTree, nodeList, includeLoadState) {
    var cachedLengthProperty = this._cachedLengthProperty,
        recursionCountProperty = this._recursionCountProperty,
        allCachedProperty = this._visibleDescendantsCachedProperty,

        newOpenProperty = newTree.openProperty,
        newIsFolderProperty = newTree.isFolderProperty,
        newCachedLengthProperty = newTree._cachedLengthProperty,
        newRecursionCountProperty = newTree._recursionCountProperty,
        newAllCachedProperty = newTree._visibleDescendantsCachedProperty;

    if (nodeList == null) {
        return null;
    } else if (isc.isAn.Array(nodeList)) {
        var newNodeList = new Array(nodeList.length);
        for (var i = nodeList.length; i--; ) {
            var oldNode = nodeList[i],
                newNode = null;
            if (oldNode != null) {

                var isOpen = this.isOpen(oldNode),
                    isFolder = this.isFolder(oldNode),
                    cachedLength = oldNode[cachedLengthProperty],
                    recursionCount = oldNode[recursionCountProperty],
                    allCached = oldNode[allCachedProperty];

                newNode = this.getCleanNodeData(oldNode, false, false, includeLoadState);

                newNode[newOpenProperty] = isOpen;
                newNode[newIsFolderProperty] = isFolder;
                newNode[newCachedLengthProperty] = cachedLength;
                newNode[newRecursionCountProperty] = recursionCount;
                newNode[newAllCachedProperty] = allCached;
            }
            newNodeList[i] = newNode;
        }
        return newNodeList;
    } else {
        var isOpen = this.isOpen(nodeList),
            isFolder = this.isFolder(nodeList),
            cachedLength = nodeList[cachedLengthProperty],
            recursionCount = nodeList[recursionCountProperty],
            allCached = nodeList[allCachedProperty],
            newNode = this.getCleanNodeData(nodeList, false, false, includeLoadState);

        newNode[newOpenProperty] = isOpen;
        newNode[newIsFolderProperty] = isFolder;
        newNode[newCachedLengthProperty] = cachedLength;
        newNode[newRecursionCountProperty] = recursionCount;
        newNode[newAllCachedProperty] = allCached;
        return newNode;
    }
},

_duplicate : function (node, newTree, newNode, includeLoadState) {


    // If the node is a leaf, return immediately since it's not going to have any children.
    if (this.isLeaf(node)) {
        return;
    }

    var children = this.getChildren(node),
        childrenLength = 0;

    if (isc.isA.ResultSet(children)) {
        childrenLength = children.getLength();
        newNode[newTree.childCountProperty] = childrenLength;
        var newChildren = newNode[newTree.childrenProperty] = new Array(childrenLength);
        for (var i = childrenLength; i--; ) {
            newChildren[i] = children.getCachedRow(i);
        }
    } else if (isc.isAn.Array(children)) {
        childrenLength = children.length;
        newNode[newTree.childrenProperty] = children;
    } else if (children) {
        childrenLength = 1;
        newNode[newTree.childrenProperty] = [children];
    }

    // Iterate through all the children of the node to put clean copies of the children under
    // the node of the new tree.
    var modelTypeParent = (this.modelType == isc.Tree.PARENT),
        parentIdField = this.parentIdField,
        newChildren = newNode[newTree.childrenProperty] = this._getCleanNodeData(
            newTree, newNode[newTree.childrenProperty], includeLoadState);
    for (var i = 0; i < childrenLength; ++i) {
        var child = children.getCachedRow(i);
        if (child == null) {
            continue;
        }
        if (modelTypeParent) {
            newChildren[i][parentIdField] = child[parentIdField];
        }

        // If the child is a folder, recurse, but check that it actually has children.
        var grandChildren = child[this.childrenProperty];
        if (grandChildren && !grandChildren.isEmpty()) {
            // Now duplicate the descendants of the child.
            this._duplicate(child, newTree, newChildren[i], includeLoadState);
        }
    }
},


destroy : function () {
    if (this.isPaged()) {
        this._cleanResultSetChildren(this.getRoot(), false);
    }

    var dataSource = isc.DataSource.getDataSource(this.dataSource);
    if (dataSource) this.ignore(dataSource, "dataChanged");
    this.Super("destroy", arguments);
},

isLocal : function () { return this.fetchMode == "local" },
isPaged : function () { return this.fetchMode == "paged" },
haveCriteria : function (criteria) {
    return !(
        criteria == null ||
        isc.isAn.emptyObject(criteria) ||
        // `isc.DataSource.convertCriteria({})` is considered empty as well:
        (criteria._constructor === "AdvancedCriteria" &&
            criteria.operator === "and" &&
            isc.isAn.Array(criteria.criteria) &&
            criteria.criteria.length == 0));
},

// This is necessary to support higher-level callback processing like for DBC.filterData.
setContext : function (context) {
    this.context = context;

    // Update the context on any ResultSet children.
    if (this.isPaged() && this._resultSetChildren != null) {
        for (var i = this._resultSetChildren.length; i--; ) {
            var newContext = context && isc.addProperties({}, context);
            if (context) {
                delete newContext.clientContext;
                delete newContext.internalClientContext;
            }
            this._resultSetChildren[i].setContext(newContext);
        }
    }
},

// A Tree navigates a 1 to many (parent to children) relationship, which can exist within or
// across DataSources.

// figuring out the type of child records at each level of the tree
// - use cases
//   - all one type
//     - supported: set just this.dataSource
//   - fixed levels
//     - example: salesOrder, lineItem
//     - supported: set this.dataSource for root DataSource, this.treeRelations for transitions
//   - mixed child types (each parent in a level has different child types)
//     - example: outlook left-hand tree navigation: top level is a random conglomeration of Inbox,
//       Favorites, etc, each with different child node types (message folders, filesystem folders,
//       etc)
//     - supported: next level is specified via node[this.childNodeType], or via overriding
//       getChildDataSource
//   - mixed type within a level
//     - supported: the Tree just needs a DataSource with a primary key for the level.  Any join
//       that can produce this is fine.

getTreeRelationship : function (parentNode) {
    var childDS = this.getChildDataSource(parentNode);
    // ask the datasource for a tree relationship, which can be declared explicitly or
    // autodetected from field declarations
    var relationship = childDS.getTreeRelationship();
    return relationship;


},

//> @method resultTree.getChildDataSource()
// Get the DataSource for children under this node.
//
// If this node has no appropriate child node type, this method will return null - in a multi-DS
// tree this indicates that there is no appropriate next DataSource to navigate to, and this node
// will be a leaf.
//<
// NOTE: nodeDS is an optional parameter, used when we need to know the child datasource of a node
// before it gets linked into the tree (at that time, the node's DS can't be determined by looking
// at it's future parent).
getChildDataSource : function (node, nodeDS) {
    // look for explicitly specified child type
    var childDSName = node[this.childTypeProperty];
    if (childDSName != null) return isc.DS.get(childDSName);

    // see if there is a mapping from this parent's type to its child type
    var nodeDS = nodeDS || this.getNodeDataSource(node);

    // - if this is a single DS tree, use the one and only DataSource
    // - if we're at root (which is the only node with no DS), use the root DataSource
    if (nodeDS == null || !this.isMultiDSTree()) return this.getRootDataSource();

    // otherwise try to find a relation from this node's DS to some other DS

    // see if there's an explicitly declared tree relation
    var treeRelations = this.treeRelations,
        childDataSources = nodeDS.getChildDataSources();

    //this.logWarn("getChildDataSource: nodeDS is : " + nodeDS +
    //             ", treeRelations: " + this.echo(treeRelations) +
    //             ", childDataSources: " + this.echo(childDataSources));

    if (treeRelations) {
        childDSName = treeRelations[nodeDS.ID];
        if (childDSName != null) return isc.DS.get(childDSName);
    }
    // otherwise take the first relationship to any other DataSource
    if (childDataSources != null) return childDataSources[0];
},

// get the DataSource for this node
getNodeDataSource : function (node) {
    // check for explicitly specified type (this allows mixed types within a set of children)
    var dsName = node[this.nodeTypeProperty];

    // use the type stored on parent node when this child was fetched
    if (dsName == null) {
        var parentNode = this.getParent(node);
        if (parentNode == null) {
            // the special, singular "root" object has no DataSource
            return null;
        } else if (parentNode == this.root) {
            // nodes under root are of the first or "root" DataSource (slightly confusing)
            dsName = this.getRootDataSource().ID;
        } else {
            // when we have a mixture of node types, and the parent stores the type of the
            // child nodes when they are loaded
            dsName = parentNode._derivedChildNodeType;
            // otherwise we have just one node type
            if (dsName == null) dsName = this.getRootDataSource().ID;
        }
    }
    return isc.DS.get(dsName) || this.getRootDataSource();
},

isMultiDSTree : function () {
    return this.multiDSTree || this.treeRelations != null;
},

// get the DataSource for the nodes that appear at root
getRootDataSource : function () {
    if (this.operation && this.operation.dataSource) return isc.DS.get(this.operation.dataSource);
    else return  isc.DS.get(this.dataSource);
},

// get the criteria to apply (aside from parentId) when selecting children from childDS
getCriteria : function (childDS, parentDS, parentNode) {
    if (this.getRootDataSource() == childDS) return this.criteria;
    return null;
},

// get an operationId to use to select children from childDS.   operation can optionally depend
// on parentDS and parentNode
getOperationId : function (childDS, parentDS, parentNode) {
    // FIXME we may want a declarative way to specify the operation to use to select on each
    // DataSource the tree may encounter
    return this.operation ? this.operation.ID : null;
},

//>    @method resultTree.loadChildren()
// @include tree.loadChildren()
//<

//>    @method resultTree.unloadChildren()
// @include tree.unloadChildren()
//<

_getRelationship : function (parentNode, debugLog) {

    // figure out what parent-child relationship will be used to select children of this node.
    var isRoot = (parentNode == null || parentNode == this.root),
        relationship;

//    if (debugLog) {
//        this.logWarn(
//            "parentNode: " + this.echo(parentNode) + ", isRoot: " + isRoot);
//    }

    // if we're at root, and this is a multi-DataSource tree, the root-level nodes have no parent
    // dataSource.  We just do a normal select, using only the criteria
    var childDS, parentDS;
    if (isRoot && this.isMultiDSTree()) {
        childDS = this.getRootDataSource();
        parentDS = null;
        // XXX this isn't really a relationship: the singular "root" has no schema, hence there is
        // no "parentDS" or "idField", and in the childDS there is no parentIdField that points to
        // root.  But the notion of "childDS", the DataSource of the nodes being loaded, is still
        // valid.
        relationship = { childDS:childDS };
    } else {
        // otherwise, we detect some relationship that this node has either within its own
        // DataSource or across DataSources, and load children using that relationship
        relationship = this.getTreeRelationship(parentNode);
        childDS = relationship.childDS;
        parentDS = relationship.parentDS;
    }

    if (!this.isMultiDSTree()) {
        // force local overrides of idField, parentIdField and rootValue on the relationship -
        // these are autodetected and initialized in init() if unset on this ResultTree.
        relationship.idField = this.idField;
        relationship.parentIdField = this.parentIdField;
        relationship.rootValue = relationship.rootValue;
    }
    if (debugLog && this.logIsDebugEnabled()) {
        this.logDebug("parent id: " + (isRoot ? "[root]" : parentNode[relationship.idField]) +
                     " (type: " + (isRoot ? "[root]" : (parentDS ? parentDS.ID : "null")) + ")" +
                     " has childDS: " + childDS.ID +
                     ", relationship: " + this.echo(relationship));
    }
    return relationship;
},


_getLoadChildrenCriteria : function (parentNode, relationship, debugLog) {

    // put together criteria that should always be used when selecting against this DataSource
    var isRoot = (parentNode == null || parentNode == this.root),
        childDS = relationship.childDS,
        parentDS = relationship.parentDS,
        criteria = {};

    if (!this.isLocal()) {
        // no local filtering - send all criteria to the server
        criteria = isc.addProperties({}, this.getCriteria(childDS, parentDS, parentNode));
    } else if (this._serverCriteria != null) {
        criteria = isc.addProperties({}, this._serverCriteria);
    }

    if (isRoot && this.isMultiDSTree()) {
        // leave criteria alone
    } else if (this.loadDataOnDemand || this.isPaged()) {
        // loadOnDemand: instead of loading the whole tree, only load the children of a single
        // node.  Put together criteria that will find all records from the childDS that belong
        // under this parent record (eg lineItems in a salesOrder)

        var parentIdFieldValue = parentNode[relationship.idField];
        // Note: If we're loading the children of the root node, default to the
        // rootValue as specified at the dataSource level if no rootValue was specified directly
        // on the tree
        var undefined;
        if (isRoot && parentIdFieldValue === undefined) {
            parentIdFieldValue = relationship.rootValue;
        }
        if (criteria._constructor == "AdvancedCriteria") {
            criteria = isc.DataSource.combineCriteria(
                criteria,
                {_constructor:"AdvancedCriteria",
                 fieldName:relationship.parentIdField,
                 value:parentIdFieldValue, operator:"equals"},
                "and"
            );
        } else {
            criteria[relationship.parentIdField] = parentIdFieldValue;
        }
        //if (debugLog) {
        //    this.logWarn("criteria is: " + isc.JSON.encode(criteria));
        //}
    }
    return criteria;
},

// Note this is an internal method to fetch the children and fold them into the children array
// for the node in question. It doesn't check for the children already being loaded - so if
// called repeatedly you'd end up with duplicates in the children array.
_loadChildren : function (parentNode, start, end, callback) {

    var relationship = this._getRelationship(parentNode, true),
        childDS = relationship.childDS,
        parentDS = relationship.parentDS;

    // remember the type of children under this parent, because we'll use this to figure out the
    // type of the children's children.
    parentNode._derivedChildNodeType = childDS.ID;

    var isRoot = (parentNode == null || parentNode == this.root),
        criteria = this._getLoadChildrenCriteria(parentNode, relationship, true);
    if (!((isRoot && this.isMultiDSTree()) || this.loadDataOnDemand)) {
        // we're going to fetch the entire tree in one go, so mark everything as loaded
        this.setDefaultLoadState(isc.Tree.LOADED);
    }

    // Remember the parentNode whose children we are loading, and what relationship we used
    // also set up the callback to fire on return.

    var internalClientContext = {
        parentNode: parentNode,
        relationship: relationship,
        childrenReplyCallback: callback
    };

    // If this is the initial fetch, hang a flag on the internalClientContext so we know to
    // fire the initial fetch callback.

    if (!this._performedInitialFetch) {
        internalClientContext._isInitialFetch = true;
        this._performedInitialFetch = true;
    }

    // Hang onto a unique fetch "id" so if invalidateCache is called before the fetch
    // returns we know the results are essentially invalid.
    var fetchCount = internalClientContext.fetchCount = (++this.currentFetch);

    var requestProperties = isc.addProperties({
        parentNode: parentNode,
        resultTree: this
    }, this.context);
    if (this.context && this.context.internalClientContext) {
        internalClientContext = isc.addProperties(
            {}, this.context.internalClientContext, internalClientContext);
    }
    requestProperties.internalClientContext = internalClientContext;

    // get an operation to do a select against the child DS
    var operationId = this.getOperationId(childDS, parentDS, parentNode);
    if (operationId) requestProperties.operationId = operationId;

    // set willHandleErrors to true so we can clear up our loading prompt on a server error
    requestProperties.willHandleError = true;


    if (this.isPaged()) {
        requestProperties.startRow = start;
        requestProperties.endRow = end;
        requestProperties.sortBy = isc.shallowClone(this._serverSortBy);

        if (this.keepParentsOnFilter) {
            requestProperties.keepParentsOnFilter = true;
        }

        if (parentNode != null && start != null && end != null) {
            var children = parentNode[this.childrenProperty];
            if (!isc.isA.ResultSet(children)) {
                // An invalidateCache() call occurring before an initial load returns from the
                // server should also invalidate this tracking of initially loading ranges.
                var loadingState = parentNode[this._initialLoadingStateProperty],
                    loadingFetchCount = parentNode[this._initialLoadingFetchCountProperty],
                    invalidated = (
                        loadingFetchCount != null &&
                        this.invalidatedFetchCount != null &&
                        loadingFetchCount <= this.invalidatedFetchCount);

                if (loadingState == null || invalidated) {
                    parentNode[this._initialLoadingFetchCountProperty] = fetchCount;
                    loadingState = parentNode[this._initialLoadingStateProperty] = isc.BitSet.create();
                    loadingState.setRange(start, end, true);
                } else {

                    if (callback == null && loadingState.all(true, start, end)) {
                        return;
                    }
                    loadingState.setRange(start, end, true);
                }
            }
        }
    }

    // set the parent as loading
    if (parentNode != null) this.setLoadState(parentNode, isc.Tree.LOADING);


    var progressiveLoading = this._getProgressiveLoading(),
        progressiveLoadingProperties = null;
    if (progressiveLoading !== false) {
        var parentOfLastNode = true;
        for (var n = parentNode, p = null; parentOfLastNode && n != null; n = p) {
            p = this.getParent(n);
            if (p == null) {
                parentOfLastNode = true;
            } else {
                var c = this.getChildren(p);
                parentOfLastNode = (c.getLength() == 1 + c.indexOf(n));
            }
        }

        progressiveLoading = parentOfLastNode && progressiveLoading;
        if (progressiveLoading === true || progressiveLoading === false) {
            progressiveLoadingProperties = { progressiveLoading: progressiveLoading };
        }
    } else {
        progressiveLoadingProperties = { progressiveLoading: false };
    }

    // kick off the operation to fetch children
    childDS.fetchData(criteria, { caller: this, methodName: "loadChildrenReply" },
        isc.addProperties(requestProperties, progressiveLoadingProperties));
},
currentFetch:0,

loadChildrenReply : function (dsResponse, data, request) {
    var context = dsResponse.internalClientContext,
        parentNode = context.parentNode;


    if (parentNode != null && this.isPaged()) {
        delete parentNode[this._initialLoadingStateProperty];
        delete parentNode[this._initialLoadingFetchCountProperty];
    }

    // If 'invalidateCache' was called while a fetch was in operation, ignore the
    // response.

    var fetchCount = context.fetchCount;
    if (this.invalidatedFetchCount != null && fetchCount <= this.invalidatedFetchCount) {
        return;
    }


    var ancestor = parentNode,
        greatAncestor;
    while ((greatAncestor = this.getParent(ancestor)) != null) {
        ancestor = greatAncestor;
    }
    if (ancestor !== this.getRoot()) {
        return;
    }

    // Are we filtering data locally?
    var localFiltering = (this.isLocal() && this.haveCriteria(this._localFilter || this.criteria));

    // incorporate the new records into the tree
    var relationship = context.relationship,
        newNodes = dsResponse.data;

    // if we're returned an error handle it as if we were returned no data, then
    // call the standard RPCManager error handling code
    if (dsResponse.status < 0) newNodes = null;

    // if we're returned the STATUS_OFFLINE condition, handle it as an empty dataset
    if (dsResponse.status == isc.RPCResponse.STATUS_OFFLINE) {
        newNodes = [];
        if (parentNode != null && !this.isRoot(parentNode)) {
            isc.say(window[request.componentId].offlineNodeMessage);
        }
    }

    // Determine target tree for this new data.
    // Re-target our parentNode if using a different tree.
    var tree = this;
    if (localFiltering) {
        // Link new data into the complete tree
        if (!this.completeTree) this.completeTree = this.duplicate(true, true);
        tree = this.completeTree;

        var parentPath = this.getPath(parentNode);
        parentNode = tree.find(parentPath);
    }

    if (!isc.isA.Array(newNodes) || newNodes.length == 0) {
        // no new nodes, mark parentNode as loaded
        if (dsResponse.status == isc.RPCResponse.STATUS_OFFLINE) {
            tree.setLoadState(parentNode, isc.Tree.UNLOADED);
            tree.delayCall("closeFolder", [parentNode], 0);
        } else {
            tree.setLoadState(parentNode, isc.Tree.LOADED);
        }

        if (!isc.isA.Array(newNodes)) {
            if (dsResponse.status < 0) {
                isc.RPCManager._handleError(dsResponse, request);
            } else if (newNodes == null) {
                this.logWarn("null children returned; return empty List instead");
            } else {
                this.logWarn("Unexpected response format, " +
                             "Array of new nodes expected, response was: " +
                             this.echoLeaf(newNodes));
            }
            newNodes = [];
        }
    }

    if (this.isPaged()) {
        var numResults = newNodes.length;

        // If the server did not specify startRow then assume that the startRow is what was asked for.
        var startRow = (dsResponse.startRow != null ? dsResponse.startRow : request.startRow);

        // If the server did not specify endRow then assume that the endRow is startRow plus the
        // number of records returned.
        var endRow = (dsResponse.endRow != null ? dsResponse.endRow : startRow + numResults);

        // If the server did not specify totalRows but the resulting endRow is less than what we
        // asked for then we know that the server does not have more rows, so set totalRows to endRow.
        var totalRows = (
                dsResponse.totalRows == null && endRow < request.endRow ?
                    endRow : dsResponse.totalRows);

        var children = parentNode[this.childrenProperty];
        if (isc.isA.ResultSet(children)) {

        } else if (endRow < totalRows) {
            parentNode[this.childCountProperty] = totalRows;
            var grandParent = this.getParent(parentNode),
                origParentLength = grandParent != null && this._getNodeLengthToParent(parentNode, grandParent);
            children = this._canonicalizeChildren(
                parentNode, parentNode[this.childrenProperty], false);


            children.fillCacheData(newNodes, startRow);
        } else {
            if (children != null) {
                var existingNodes = children.getRange(startRow, endRow);
                for (var i = existingNodes.length; i--; ) {
                    this._remove(existingNodes[i]);
                }
            }
            this._addList(newNodes, parentNode, startRow);
        }
    } else if (tree.isMultiDSTree()) {
        for (var i = 0; i < newNodes.length; i++) {
            var node = newNodes[i];
            // in a multi-DS tree, a node is a folder if there's a childDS to fetch nodes from
            var nextChildDS = this.getChildDataSource(node, relationship.childDS);
            if (nextChildDS != null) this.convertToFolder(node);

            // Node naming:
            // - in a single-DS tree, all nodes have an id that is unique tree-wide, the "idField" from
            //   the tree relationship
            // - in a multi-DS tree, nodes are from a mix of DataSources and do not necessarily have a
            //   tree-wide unique id - they only have a unique id within each set of children, since
            //   each set of children can be from a different DataSource (even when on the same level).
            //
            // So, for multiDSTrees, in this case all newNodes are immediate children
            //
            // link it in
            tree._add(node, parentNode);
        }
    } else {
        // we're dealing with a mixed bag of parents and children, any number of levels deep.  In
        // this case we assume a unique id across all tree nodes, as opposed to just one level, and
        // run a linking algorithm that can handle the nodes in any order.

        if (dsResponse.status == isc.RPCResponse.STATUS_OFFLINE) {
            tree.setLoadState(parentNode, isc.Tree.UNLOADED);
            tree.delayCall("closeFolder", [parentNode], 0);
        } else {
            // If we are filtering locally, postpone dataChanged event until we finish filtering.
            var suppressDataChanged = localFiltering;
            tree._linkNodes(newNodes,
                           relationship.idField,
                           relationship.parentIdField,
                           relationship.rootValue,
                           relationship.isFolderProperty,
                           parentNode,
                           suppressDataChanged);
        }
    }
    // If filtering locally, do it now.
    if (localFiltering) {
        // If we didn't set up the "openStateForLoad" flag, do it now.
        if (this._openStateForLoad == null) this._openStateForLoad = this._getOpenState();
        this.filterLocalData();
    }
    // Reopen any nodes after cache is filled
    if (this._openStateForLoad) {
        this._setOpenState(this._openStateForLoad, true);
        delete this._openStateForLoad;
    }

    // Fire any callback passed to 'loadChildren' in the scope of this tree.
    if (context.childrenReplyCallback) {
        this.fireCallback(context.childrenReplyCallback, "node", [parentNode], this);
    }

    // NOTE: when paging within child sets is implemented, we'll add "startChild,endChild" to
    // this signature
    if (this.dataArrived != null) {
        isc.Func.replaceWithMethod(this, "dataArrived", "parentNode");
        this.dataArrived(parentNode);
    }
},


// Cache sync
// ------------------------------------
// On initial load of data for some folder, we always retrieve the entire set of children for the
// parents of the node.
// When dataChanged fires on our dataSource, we need to update these stored children arrays to
// incorporate the modified nodes into our tree of local data.

// helper method to get this.dataSource as a datasource object (even if specified as an ID only)
getDataSource : function () {
    return isc.DataSource.getDataSource(this.dataSource);
},

//> @method resultTree.invalidateCache() [A]
// Manually invalidate this ResultTree's cache.
// <P>
// Generally a ResultTree will observe and incorporate updates to the DataSource that provides its
// records, but when this is not possible, <code>invalidateCache()</code> allows manual cache
// invalidation.
// <P>
// Components bound to this ResultTree will typically re-request the currently visible portion
// of the dataset, causing the ResultTree to re-fetch data from the server.
// @visibility external
//<
invalidateCache : function () {
    if (!this.isLoaded(this.root)) return;

    // Ensure that if a current fetch is in progress, we ignore its response in favor
    // of the new results.
    this.invalidatedFetchCount = this.currentFetch;

    // Save current open state so it can be reapplied when new data arrives
    this._openStateForLoad = this._getOpenState(true);
    // reset autoName to zero.

    this._autoName = 0;

    // Reset root to refetch all our data.
    this.setRoot(this.makeRoot(), true);


    if (!this.loadDataOnDemand) {
        this.reloadChildren(this.root);
    }
},

dataSourceDataChanged : function (dsRequest, dsResponse) {

    // respsect the flag to suppress cache sync altogether
    if (this.disableCacheSync) return;

    var updateData = isc.DataSource.getUpdatedData(dsRequest, dsResponse,
                                                   this.updateCacheFromRequest);

    this.handleUpdate(dsRequest.operationType, updateData, dsResponse.invalidateCache);
},

handleUpdate : function (operationType, updateData, forceCacheInvalidation) {
    if (isc._traceMarkers) arguments.__this = this;

    var dropCacheOnUpdate = (
            this.dropCacheOnUpdate || forceCacheInvalidation ||

            (this.isPaged() && this.keepParentsOnFilter));
    if (dropCacheOnUpdate) {


        this.invalidateCache();


        if (!this.getDataSource().canQueueRequests) this.dataChanged();
        return;
    }

    // update our cached tree directly  Note our cache is filtered, so we may just discard the
    // update if the new row doesn't pass the filter
    this.updateCache(operationType, updateData);
    this.dataChanged();
},


// updateCache() - catch-all method fired when the dataSource dataChanged method fires.
// Integrates (or removes) the modified nodes into our local tree of data.
updateCache : function (operationType, updateData) {
    if (updateData == null) return;


    operationType = isc.DS._getStandardOperationType(operationType);

    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    //>DEBUG
    if (this.logIsInfoEnabled()) {
        this.logInfo("Updating cache: operationType '" + operationType + "', " +
                     updateData.length + " rows update data" +
                     (this.logIsDebugEnabled() ?
                      ":\n" + this.echoAll(updateData) : ""));
    } //<DEBUG

    switch (operationType) {
    case "remove":
        this.removeCacheData(updateData);
        break;
    case "add":
        this.addCacheData(updateData);
        break;
    case "replace":
    case "update":
        this.updateCacheData(updateData);
        break;
    }

},

addCacheData : function (updateData) {
    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    var checkParent = (this.idField != undefined && this.parentIdField != undefined),
        pk = this.getDataSource().getPrimaryKeyFieldNames()[0];



    // if we've got a completeTree, add all the new nodes to its cache
    if (this.completeTree) for (var i = 0; i < updateData.length; i++) {
        var addRow = updateData[i];


        if (checkParent && addRow != null && addRow[this.idField] == addRow[this.parentIdField])
        {
            this.logWarn(
                "Invalid attempt to add a node that is specified to be its own parent " +
                "(the '" + this.idField + "' + and '" + this.parentIdField + "' properties " +
                "of the node are both set to " + (addRow[this.idField] == null ? "null" :
                addRow[this.idField].toString()) + ").  Skipping this node.");
            continue;
        }
        this._addNodeToCache(this.completeTree, addRow, pk);
    }

    // now turn to visible tree - remove any rows of new data that don't pass filtering
    var validRows = this.getDataSource().applyFilter(updateData, this.criteria, this.context);

    this.logInfo("Adding rows to cache: " + validRows.length + " of " + updateData.length +
                 " rows match filter criteria");

    for (var i = 0; i < validRows.length; i++) {
        var addRow = validRows[i];


        if (checkParent && addRow != null && addRow[this.idField] == addRow[this.parentIdField])
        {
            if (!this.completeTree) this.logWarn(
                "Invalid attempt to add a node that is specified to be its own parent " +
                "(the '" + this.idField + "' + and '" + this.parentIdField + "' properties " +
                "of the node are both set to " + (addRow[this.idField] == null ? "null" :
                addRow[this.idField].toString()) + ").  Skipping this node.");
            continue;
        }
        this._addNodeToCache(this, addRow, pk);
    }
},

_addNodeToCache : function (tree, node, pk) {


    var parentId = node[this.parentIdField], parentNode;

    if (parentId != null) parentNode = tree.find(pk, parentId);
    else {
        if (this.defaultNewNodesToRoot || tree.rootValue == null) parentNode = tree.getRoot();
        else parentNode = null;
    }

    // Duplicate the node when adding it -- this is required to avoid us writing
    // properties onto the object directly
    // Note: _add() will automatically sort the new node in the children array
    var addNode = (
            parentNode != null &&
            (tree.getLoadState(parentNode) == isc.Tree.LOADED) &&

            !(this.isPaged() && isc.isA.ResultSet(this.getChildren(parentNode))));
    if (addNode) {
        node = isc.clone(node);
        tree._add(node, parentNode);
    }
    return addNode;
},

updateCacheData : function (updateData) {
    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    //>DEBUG
    var debugTotals = {
        addedRows: 0,
        updatedRows: 0,
        removedRows: 0};
    //<DEBUG

    // Are we filtering data locally?
    var criteria = (this._localCriteria || this.criteria),
        haveCriteria = this.haveCriteria(criteria);


    var checkParent = (this.idField != undefined && this.parentIdField != undefined),
        keepParents = this.keepParentsOnFilter && haveCriteria,
        ds = this.getDataSource();

    for (var i = 0; i < updateData.length; i++) {
        var updateRow = updateData[i];
        var matchesFilter = true;
        if (haveCriteria) {
            var matches = ds.applyFilter([updateRow], criteria, this.context);
            matchesFilter = (matches != null && matches.length > 0);
        }
        //>DEBUG
        if (this.logIsDebugEnabled() && !matchesFilter) {
            this.logDebug("updated node :\n" + this.echo(updateRow) +
                         "\ndidn't match filter: " + this.echo(criteria));
        }
        //<DEBUG

        if (updateRow == null) {
            continue;
        }


        if (checkParent &&
            updateRow[this.idField] == updateRow[this.parentIdField])
        {
            this.logWarn(
                "Invalid attempt to update a node where the '" + this.idField + "' and '" +
                this.parentIdField + "' properties of the record are both set to " +
                (updateRow[this.idField] == null ? "null" : updateRow[this.idField].toString()) +
                " (this would reparent the node to itself).  Skipping this node.");
            continue;
        }

        // Update cache of the entire tree (all nodes)
        if (this.completeTree) {
            this._updateNodeInCache(this.completeTree, updateRow, true);
        }
        // Update the visible tree
        this._updateNodeInCache(this, updateRow, matchesFilter, keepParents
        //>DEBUG
        , debugTotals
        //<DEBUG
        );
    }

    //>DEBUG
    if (this.logIsDebugEnabled()) {
        this.logDebug("updated cache: "
             + debugTotals.addedRows + " row(s) added, "
             + debugTotals.updatedRows + " row(s) updated, "
             + debugTotals.removedRows + " row(s) removed.");
    }
    //<DEBUG
},

_updateNodeInCache : function (tree, updateRow, matchesFilter, keepParents, debugTotals) {

    var ds = this.getDataSource(),
        pk = ds.getPrimaryKeyFieldNames()[0],
        node = tree.find(pk, updateRow[pk]);

    // Very likely we'll see null nodes - we probably haven't opened their parent folder yet
    // However - check for the case where we have and if so, add to our data-set
    if (node == null) {
        if (matchesFilter) {
            if (this._addNodeToCache(tree, updateRow, pk)) {
                // This situation is valid - a developer updated a child of a parent we haven't
                // loaded (possibly in another tree on the page) and shifted it into a
                // parent we have loaded
                this.logInfo("updated row returned by server doesn't match any cached row, " +
                             " adding as new row.  Primary key value: " + this.echo(updateRow[pk]) +
                             ", complete row: " + this.echo(updateRow));
                if (debugTotals) debugTotals.addedRows++;
            }
        }
        return;
    }


    if (keepParents && !matchesFilter) {
        var children = node[tree.childrenProperty];
        if (children != null && !children.isEmpty()) {
            matchesFilter = true;
        }
    }


    var paged = this.isPaged(),
        prevSiblings = paged && this.getChildren(this.getParent(node));
    if (matchesFilter) {
        // the change may have reparented a node.
        if (updateRow[this.parentIdField] != node[this.parentIdField]) {
            var newParentNode = tree.find(pk, updateRow[this.parentIdField]);
            if (newParentNode == null &&
                (this.defaultNewNodesToRoot || this.rootValue == null))
            {
                newParentNode = tree.getRoot();
            }

            if (newParentNode == null || (tree.getLoadState(newParentNode) != isc.Tree.LOADED)) {
                if (!(paged && isc.isA.ResultSet(prevSiblings))) {
                    tree._remove(node);
                    if (debugTotals) debugTotals.removedRows++;
                }
                return;
            } else {
                var newSiblings = paged && this.getChildren(newParentNode),
                    add = !(paged && isc.isA.ResultSet(newSiblings)),
                    remove = !(paged && isc.isA.ResultSet(prevSiblings));
                if (add && remove) {
                    tree._move(node, newParentNode);
                } else if (add) {
                    tree._add(node, newParentNode);
                } else if (remove) {
                    tree._remove(node);
                }
            }
        }
        // apply all modified fields to the node.
        isc.addProperties(node, updateRow);


        var fieldNames = ds.getFieldNames();
        for (var i = 0; i < fieldNames.length; i++) {
            var name = fieldNames[i];
            if (!updateRow.hasOwnProperty(name)) delete node[name];
        }
        if (debugTotals) debugTotals.updatedRows++;
    } else if (!(paged && isc.isA.ResultSet(prevSiblings))) {
        tree._remove(node);
        if (debugTotals) debugTotals.removedRows++;
    }
},

removeCacheData : function (updateData) {
    if (!isc.isAn.Array(updateData)) updateData = [updateData];

    var pk = this.getDataSource().getPrimaryKeyFieldNames()[0];

    // Update cache of the entire tree (all nodes)

    if (this.completeTree) {
        this._removeNodesFromCache(this.completeTree, updateData, pk);
    }
    // Update the visible tree
    this._removeNodesFromCache(this, updateData, pk);
},

_removeNodesFromCache : function (tree, updateData, pk) {


    // Build list of nodes to be removed
    var paged = this.isPaged(),
        nodes = [];
    for (var i = 0; i < updateData.length; i++) {
        var node = tree.find(pk, updateData[i][pk]);
        if (node == null) {
            this.logWarn("Cache synch: couldn't find deleted node:" + this.echo(updateData[i]));
        } else if (!(paged && isc.isA.ResultSet(this.getChildren(this.getParent(node))))) {
            nodes.add(node);
        }
    }
    tree._removeList(nodes);
},

// get the title for this node
getTitle : function (node) {
    // look up the node's DataSource and return its title field
    var dataSource = this.getNodeDataSource(node);

    // the special, singular root node has no DataSource
    if (!dataSource) return "root";

    var title = node[dataSource.getTitleField()];
    if (title != null) return title;

    // if there's no title on this node, try not to leave a blank
    return this.Super("getTitle", arguments);
},

// indexOf: As with ResultSets support being passed primaryKey values only as well as pointers
// to nodes
// Note: This will return the index wrt the visible (open) nodes of the tree. If the node is not
// currently visible, -1 will be returned.
indexOf : function (node, pos, endPos, c, d) {
    var pks = this.getDataSource().getPrimaryKeyFieldNames();
    for (var i = 0; i < pks.length; i++) {
        var pk = pks[i];
        // locate the index with the node's primary key, but don't ever perform a fetch
        if (node[pk] != null) return this.findNextIndex(pos, pk, node[pk], endPos, true);
    }

    return this.invokeSuper(isc.ResultTree, "indexOf", node, pos, endPos, c, d);
},


contains : function (node, pos, comparator) {
    if (this.isPaged() && node != null && pos === undefined && comparator === undefined) {
        var idProperty = this.idField;
        return (node[this.treeProperty] == this.ID && this.nodeIndex[node[idProperty]] == node);
    } else {
        return this.invokeSuper(isc.ResultTree, "contains", node, pos, comparator);
    }
},




getLength : function () {
    var length = this.invokeSuper(isc.ResultTree, "getLength");
    if (this.isPaged() && this._resultSetChildren != null) {
        var root = this.getRoot(),
            defaultChildLength = (
                this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0);

        for (var i = this._resultSetChildren.length; i--; ) {
            var children = this._resultSetChildren[i],
                parent = children._parentNode,
                openSubfoldersAllowed = (
                    parent[this.canReturnOpenSubfoldersProperty] != null ?
                    parent[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
                knownLengthNulls = !(openSubfoldersAllowed || defaultChildLength == 0);
            if (!knownLengthNulls) {
                var visible = true;
                for (var p = parent; visible && p != root; p = this.getParent(p)) {
                    visible = p != null && this.isOpen(p);
                }
                if (visible) {
                    length += Math.max(
                        0, children._getCachedLength() - children._getCachedRows());
                }
            }
        }
    }
    return length;
},

//> @method resultTree.get()
// @include tree.get()
// @visibility external
//<
get : function (pos, dontFetch) {
    if (pos < 0) {
        return undefined;
    } else {
        return this.getRange(pos, pos + 1, !!dontFetch)[0];
    }
},

// Override Tree.getCachedRow() to get a record at the given position without causing a fetch
// if the record has not yet been loaded.  See ResultSet.getCachedRow().
getCachedRow : function (pos) {
    if (pos < 0) {
        return null;
    } else {
        var record = this.getRange(pos, pos + 1, true)[0];
        if (record != null && record !== isc.ResultTree.getLoadingMarker()) {
            return record;
        } else {
            return null;
        }
    }
},

//> @method resultTree.getRange()
// @include tree.getRange()
// @visibility external
//<
_$getRangeInfoObj: {},
getRange : function (start, end, dontFetch) {

    if (start == null) {
        this.logWarn("getRange() called with no specified range - ignoring.");
        return;
    } else if (end == null) {
        end = start + 1;
    }
    if (start < 0 || end < 0) {
        //>DEBUG
        this.logWarn(
            "getRange(" + start + ", " + end + "): negative indices not supported, clamping " +
            "start to 0");
        //<DEBUG
        start = Math.max(start, 0);
    }
    if (end <= start) {
        //>DEBUG
        this.logDebug("getRange(" + start + ", " + end + "): returning empty list");
        //<DEBUG
        return [];
    }



    var cachedRanges = this._cachedRanges;
    if (cachedRanges != null && cachedRanges.length > 0) {
        var cachedRange = cachedRanges[cachedRanges.length - 1],
            start0 = cachedRange.start;
        if (start0 <= start && end <= cachedRange.end) {
            return cachedRange.range.slice(start - start0, end - start0);
        }
    }

    if (!this.isPaged()) {
        return this.invokeSuper(isc.ResultTree, "getRange", start, end);
    }

    var loadingMarker = isc.ResultTree.getLoadingMarker(),
        cachedLengthProperty = this._cachedLengthProperty,
        root = this.getRoot(),
        info = {
            range: [],
            rangeLoading: false,
            needQueue: false,
            wasAlreadyQueuing: false,
            dontFetch: dontFetch === true
        }
    ;

    if (end > start) {

        if (!(this.openDisplayNodeType != isc.Tree.LEAVES_ONLY && this.showRoot)) {
            // The root is not visible in the open list.  The length of the root includes 1
            // for the root, so shift the requested range by 1 to implement hiding of the root.
            ++start;
            ++end;
        }
        var progressiveLoading = this._getProgressiveLoading();
        this._getRange(root, root, [root], 0, start, end, true, progressiveLoading, false, info);

    }
    return info.range;
},

_pushCachedRange : function (start, end) {

    if (this.isPaged()) {
        if (this._cachedRanges == null) {
            this._cachedRanges = [];
        }
        this._cachedRanges.push({
            start: start,
            end: end,
            range: this.getRange(start, end)
        });
    }
},

_popCachedRange : function (start, end) {
    if (this.isPaged()) {

        this._cachedRanges.pop();
        if (this._cachedRanges.length == 0) {
            delete this._cachedRanges;
        }
    }
},


_getRange : function (root, node, children, i, start, end, recursionTopLevel, progressiveLoading, subrangeLoading, info) {

    var separateFolders = this.separateFolders,
        foldersBeforeLeaves = separateFolders && this.sortFoldersBeforeLeaves,
        leavesBeforeFolders = separateFolders && !this.sortFoldersBeforeLeaves,
        range = info.range,
        rangeLoading = info.rangeLoading,
        needQueue = info.needQueue,
        dontFetch = info.dontFetch;



    var defaultChildLength = (this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0);

    var allCachedProperty = this._visibleDescendantsCachedProperty,
        cachedLengthProperty = this._cachedLengthProperty,
        treeLoadingMarker = isc.ResultTree.getLoadingMarker(),
        openSubfoldersAllowed = (
            node[this.canReturnOpenSubfoldersProperty] != null ?
            node[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
        childrenResultSet = isc.isA.ResultSet(children),

        j = i;

    var length = (children == null ? 0 :
            (childrenResultSet ? children._getCachedLength() : children.getLength()));


    for (var z = 0; z < (separateFolders ? 2 : 1); ++z) {
        var firstPass = (z == 0),
            justFolders = (firstPass == foldersBeforeLeaves),
            justLeaves = (firstPass == leavesBeforeFolders),
            startedSubrange = false,
            subrangeStart = 0,
            p = 0;

        for ( ; j < end && p < length; ++p) {



            var child = children.getCachedRow(p);

            var cachedFlag = (
                child != null && (!this.isOpen(child) || child[allCachedProperty]));

            if (child == null) {

                var firstThreeConditions = (
                    openSubfoldersAllowed || rangeLoading || defaultChildLength == 0);
                if (firstPass && (firstThreeConditions || separateFolders || j >= start)) {
                    if (firstThreeConditions) {
                        if (!rangeLoading) {
                            // Fill in the rest of the range with LOADING markers.
                            for (var r = Math.max(start, j); r < end; ++r) {
                                range[r - start] = treeLoadingMarker;
                            }
                            rangeLoading = info.rangeLoading = true;
                            if (dontFetch) {
                                return;
                            }
                        }
                    } else {

                        subrangeLoading = separateFolders;

                        if (j >= start) {
                            range[j - start] = treeLoadingMarker;
                        }
                    }


                    if (!(dontFetch || startedSubrange)) {
                        startedSubrange = true;
                        subrangeStart = p;
                        if (!needQueue) {
                            info.wasAlreadyQueuing = isc.RPCManager.startQueue();
                            needQueue = info.needQueue = true;
                        }
                    }
                }


                j += (separateFolders && firstPass ? 0 : defaultChildLength);

            } else {

                if (startedSubrange) {

                    startedSubrange = false;
                    var prevProgressiveLoading = children.progressiveLoading;
                    children.progressiveLoading = false;
                    children.getRange(subrangeStart, p);
                    children.progressiveLoading = prevProgressiveLoading;
                }


                if (this.isFolder(child) ? !justLeaves : !justFolders) {
                    var childLength, visibleChild;
                    if (child == root) {
                        childLength = child[cachedLengthProperty];
                        visibleChild = true;
                    } else {
                        childLength = this._getNodeLengthToParent(child, node);
                        visibleChild = this._isNodeVisibleToParent(child, node);
                    }
                    var descendantsLength = childLength - (visibleChild ? 1 : 0);
                    if (j >= start && visibleChild && !rangeLoading) {
                        range[j - start] = (subrangeLoading ? treeLoadingMarker : child);
                    }
                    var k = j + (visibleChild ? 1 : 0),
                        l = j + childLength;
                    if (k < end && (!cachedFlag || (l >= start && descendantsLength > 0))) {
                        var grandchildren = this.getChildren(child);
                        this._getRange(
                            root, child, grandchildren,
                            k, start, end, false,
                            // Only allow progressive loading mode to be utilized for
                            // loading the last node of the tree or its siblings.
                            (p == length - 1 && progressiveLoading),
                            subrangeLoading,
                            info);
                        rangeLoading = info.rangeLoading;
                        needQueue = info.needQueue;
                        if (dontFetch && rangeLoading) {
                            return;
                        }
                    }
                    j = l;
                }
            }
        }

        if (startedSubrange) {

            startedSubrange = false;
            var prevProgressiveLoading = children.progressiveLoading;
            children.progressiveLoading = progressiveLoading;
            children.getRange(subrangeStart, p);
            children.progressiveLoading = prevProgressiveLoading;
        }
    }


    if (length == 0 || (childrenResultSet && !children.lengthIsKnown())) {
        if (!rangeLoading) {
            // Fill in the rest of the range with LOADING markers.
            for (var r = Math.max(start, j); r < end; ++r) {
                range[r - start] = treeLoadingMarker;
            }
            rangeLoading = info.rangeLoading = true;
            if (dontFetch) {
                return;
            }
        }

        if (!needQueue) {
            info.wasAlreadyQueuing = isc.RPCManager.startQueue();
            needQueue = info.needQueue = true;
        }

        // Copy the progressiveLoading setting onto the ResultSet for this request only.
        var prevProgressiveLoading;
        if (childrenResultSet) {
            prevProgressiveLoading = children.progressiveLoading;
            children.progressiveLoading = progressiveLoading;
        }


        if (separateFolders || (defaultChildLength == 0)) {

            if (childrenResultSet) {
                children._fetchAllRemoteData();
            } else {
                this._loadChildren(node, 0, this.resultSize, null);
            }
        } else if (childrenResultSet) {
            children.getRange(0, end - j);
        } else {
            this._loadChildren(node, 0, end - j, null);
        }

        // Restore the original progressiveLoading setting.
        if (childrenResultSet) {
            children.progressiveLoading = prevProgressiveLoading;
        }
    }

    if (recursionTopLevel && needQueue && !info.wasAlreadyQueuing) {

        isc.RPCManager.sendQueue();
    }
},


_getProgressiveLoading : function () {
    return (this.isPaged() && this.progressiveLoading);
},


_canonicalizeChildren : function (node, children, alreadyInitialized) {



    if (isc.isA.ResultSet(children)) {
        node[this.childrenProperty] = children;
        return children;
    } else if (this._linkingNodes === true) {
        return children;
    }

    var childCount = node[this.childCountProperty],
        numChildren = (isc.isAn.Array(children) ? children.length : 0),
        validChildCount = (
            childCount != null &&
            isc.isA.Number(childCount) &&
            Math.floor(childCount) == childCount &&
            childCount >= 0);

    //>DEBUG
    if (validChildCount && childCount < numChildren) {
        isc.logWarn(
            "The child count of a record was set to " + childCount + ".  The number of " +
            "children was " + numChildren + ".  The number of children cannot exceed the " +
            "child count.  Clamping the number of children to the child count.");

        children = node[this.childrenProperty] = children.slice(0, childCount);
    }
    //<DEBUG

    if (validChildCount) {


        // Define a list of all of the ResultSets of children.
        this._resultSetChildren = this._resultSetChildren || [];

        if (this.dataArrived != null) {
            isc.Func.replaceWithMethod(this, "dataArrived", "parentNode");
        }


        var openNormalizer = this._openNormalizer,
            sortSpecifiers = this._sortSpecifiers,
            clonedSortSpecifiers = false;
        if (isc.isAn.Array(sortSpecifiers) && openNormalizer != null) {
            for (var i = sortSpecifiers.length; i--; ) {
                var sort = sortSpecifiers[i];
                if (sort != null && sort.normalizer === openNormalizer) {
                    if (!clonedSortSpecifiers) {
                        sortSpecifiers = sortSpecifiers.duplicate();
                        clonedSortSpecifiers = true;
                    }
                    sortSpecifiers[i] = isc.addProperties({}, sort, { normalizer: null });
                }
            }
        }


        var dataSource = this.getDataSource(),
            initialData;
        if (this.keepParentsOnFilter) {
            initialData = (children ? children.duplicate() : []);
        } else {
            initialData = dataSource.applyFilter(children, this.criteria, this.context);
        }
        var initialLength = Math.max(
                initialData.length,
                node[this.childCountProperty] + (initialData.length - numChildren));

        var context = this.context && isc.addProperties({}, this.context);
        if (context) {
            delete context.clientContext;
            delete context.internalClientContext;
        }

        // If keepParentsOnFilter is enabled then the same-named flag is also set on all
        // DSRequests issue by the tree (including all DSRequests issued by any ResultSet of
        // children nodes in the tree).
        if (this.keepParentsOnFilter) {
            context = context || {};
            context.keepParentsOnFilter = true;
        }

        var resultSetConfig = {
            init : function () {
                var tree = this._tree;
                this._parentNode[tree.childrenProperty] = this;

                // Add this ResultSet to the list of ResultSet children.
                if (!tree._resultSetChildren.contains(this)) {
                    tree._resultSetChildren.add(this);
                }

                if (tree.dataArrived != null) {
                    tree.observe(
                        this, "dataArrived", "observer.dataArrived(observed._parentNode)");
                }

                // Cache criteria that enforces the tree relationship.
                var node = this._parentNode,
                    relationship = tree._getRelationship(node, false);
                this._loadChildrenCriteria = tree._getLoadChildrenCriteria(
                        node, relationship, false);

                var ret = this.Super("init", arguments);


                var parent = this._parentNode,
                    grandParent = parent != tree.root && tree.getParent(parent),
                    openSubfoldersAllowed = (
                        parent[tree.canReturnOpenSubfoldersProperty] != null ?
                        parent[tree.canReturnOpenSubfoldersProperty] : tree.canReturnOpenFolders),
                    defaultChildLength = (
                        tree.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0),
                    knownLengthNulls = !(openSubfoldersAllowed || defaultChildLength == 0);
                if (knownLengthNulls) {
                    var origLength = (
                            grandParent &&
                            tree._getNodeLengthToParent(parent, grandParent));
                    parent[tree._cachedLengthProperty] = tree._getNodeLength(parent);
                    if (grandParent) {
                        var deltaLength = (
                                tree._getNodeLengthToParent(parent, grandParent) - origLength);
                        tree._updateParentLengths(grandParent, deltaLength);
                    }
                }


                if (this._alreadyInitialized) {
                    this.addProperties(tree._childrenResultSetProperties);
                }
                delete this._alreadyInitialized;

                return ret;
            },


            _tree: this,
            _parentNode: node,

            _alreadyInitialized: alreadyInitialized,

            dataSource: dataSource,
            criteria: this.criteria,
            context: context,
            disableCacheSync: this.disableCacheSync,
            updateCacheFromRequest: this.updateCacheFromRequest,
            sortSpecifiers: sortSpecifiers,

            initialData: initialData,
            initialLength: initialLength,


            setCriteria : function (newCriteria) {
                var tree = this._tree;
                newCriteria = arguments[0] = tree._combineCriteria(
                    newCriteria, this._loadChildrenCriteria);
                return this.Super("setCriteria", arguments);
            }
        };

        if (this.keepParentsOnFilter) {

            resultSetConfig.dropCacheOnUpdate = true;
        }

        if (!alreadyInitialized) {
            isc.addProperties(resultSetConfig, this._childrenResultSetProperties);
        }

        children = node[this.childrenProperty] = isc.ResultSet.create(resultSetConfig);

        // The ResultSet currently has only a partial cache of the children of the node, so
        // mark the node as partially loaded.
        this.setLoadState(node, isc.Tree.LOADED_PARTIAL_CHILDREN);

        this._setVisibleDescendantsCached(node, false, null, false);


    }
    return children;
},


_combineCriteria : function (criteria1, criteria2) {
    var criteria1Null = (criteria1 == null || isc.isAn.emptyObject(criteria1)),
        criteria2Null = (criteria2 == null || isc.isAn.emptyObject(criteria2));
    if (criteria1Null || criteria2Null) {
        if (!criteria1Null) {
            return criteria1;
        } else if (!criteria2Null) {
            return criteria2;
        } else {
            return (criteria1 || criteria2);
        }
    }

    var advanced = (
            criteria1._constructor == "AdvancedCriteria" ||
            criteria2._constructor == "AdvancedCriteria");
    if (!advanced) {
        var cloned = false;
        for (var key in criteria1) {
            if (criteria2[key] != undefined) {
                if (criteria2[key] !== criteria1[key]) {
                    advanced = true;
                } else {
                    if (!cloned) {
                        criteria2 = isc.addProperties({}, criteria2);
                        cloned = true;
                    }
                    delete criteria2[key];
                }
            }
        }
    }

    var combinedCriteria = isc.DataSource.combineCriteria(criteria1, criteria2, "and");

    advanced = (combinedCriteria._constructor == "AdvancedCriteria");

    // Fix instances of { operator: "iContains", value: null } in advanced
    // criteria to use the "isNull" operator instead.
    if (advanced && isc.isAn.Array(combinedCriteria.criteria)) {
        var subcriteria = combinedCriteria.criteria;
        for (var i = subcriteria.length; i--; ) {
            var subcriterion = subcriteria[i];
            if (subcriterion.operator == "iContains" && subcriterion.value == null) {
                subcriterion.operator = "isNull";
                delete subcriterion.value;
            }
        }
    }

    return combinedCriteria;
},

// Override Tree.setSort() to call setSort() on any ResultSets of children in paged trees.
setSort : function (sortSpecifiers) {
    var ret = this.invokeSuper(isc.ResultTree, "setSort", sortSpecifiers);
    sortSpecifiers = this._sortSpecifiers; // set by Tree.setSort()

    // update sortBy parameter (added to DSRequests when children are fetched)
    this._serverSortBy = sortSpecifiers ? isc.DS.getSortBy(sortSpecifiers) : null;


    this._getOpenList();

    if (this.isPaged()) {
        var resultSetChildren = this._resultSetChildren;

        if (resultSetChildren != null && resultSetChildren.length > 0) {
            var wasAlreadyQueuing = isc.RPCManager.startQueue();

            for (var i = resultSetChildren.length; i--; ) {
                var children = resultSetChildren[i];
                children.setSort(sortSpecifiers);
            }

            if (!wasAlreadyQueuing) {
                isc.RPCManager.sendQueue();
            }
        }
    }

    return ret;
},

_getStartRow : function (node) {
    if (node == null) {
        return null;
    }

    var root = this.getRoot(),
        parent = null,
        start = 0;

    for ( ; node != root; node = parent) {
        parent = this.getParent(node);
        if (parent != null) {
            if (!this._includeNodeLengthInParent(node, parent)) {
                return null;
            }

            var children = this.getChildren(parent),
                j = children.indexOf(node);

            if (j == -1) {
                return null;
            }

            for (var i = 0; i < j; ++i) {
                var child = children.getCachedRow(i);
                if (child != null) {
                    start += this._getNodeLengthToParent(child, parent);
                }
            }

            if (this._isNodeVisibleToParent(node, parent)) {
                ++start;
            }
        } else if (node != root) {
            // The node is not actually in the tree!
            return undefined;
        }
    }

    return start;
},

_setVisibleDescendantsCached : function (node, newAllCached, parent, recalc) {

    var allCachedProperty = this._visibleDescendantsCachedProperty,
        prevAllCached = node[allCachedProperty];

    if (newAllCached == null) {


        // UNLOADED, LOADING, and FOLDERS_LOADED LoadStates cause the allCachedProperty to be
        // false.
        var loadState = this.getLoadState(node);
        newAllCached = (
            loadState === isc.Tree.LOADED ||
            loadState === isc.Tree.LOADED_PARTIAL_CHILDREN);


        var children = this.getChildren(
                node, undefined, undefined, undefined, undefined, undefined, true, undefined),
            childrenResultSet = isc.isA.ResultSet(children);

        if (newAllCached && (childrenResultSet || isc.isAn.Array(children))) {
            newAllCached = newAllCached && (
                (!childrenResultSet || (
                    children.allMatchingRowsCached() && children.lengthIsKnown())));

            for (var i = 0, length = children.getLength(); newAllCached && i < length; ++i) {
                var child = children.getCachedRow(i);
                newAllCached = (child != null && (
                    !this.isOpen(child) || child[allCachedProperty] || false));
            }
        }
    }


    node[allCachedProperty] = newAllCached;


    parent = parent || this.getParent(node);
    if ((recalc || !prevAllCached) && newAllCached) {

        if (parent != null && this.isOpen(node)) {
            var loadState = this.getLoadState(parent),
                children = this.getChildren(parent),
                childrenResultSet = isc.isA.ResultSet(children),
                allCached = (
                    (loadState === isc.Tree.LOADED ||
                        loadState === isc.Tree.LOADED_PARTIAL_CHILDREN) &&
                    (!childrenResultSet || (
                        children.allMatchingRowsCached() && children.lengthIsKnown())));

            if (allCached && (childrenResultSet || isc.isAn.Array(children))) {
                var index = children.indexOf(node);


                for (var i = children.getLength(); allCached && i-- > 0; ) {
                    if (i != index) {
                        var child = children.getCachedRow(i);
                        allCached = (child != null &&
                            (!this.isOpen(child) || child[allCachedProperty]));
                    }
                }
            }

            if (allCached) {
                this._setVisibleDescendantsCached(parent, true, null, false);
            } else {

            }
        }
    } else if ((recalc || prevAllCached) && !newAllCached) {

        while (parent != null && parent[allCachedProperty] && this.isOpen(node)) {
            parent[allCachedProperty] = false;
            node = parent;
            parent = this.getParent(parent);
        }
    }
},


__add : function (node, parent, position) {
    if (this.isPaged() && !this.keepParentsOnFilter) {
        var validRows = this.getDataSource().applyFilter([node], this.criteria, this.context);
        if (validRows.length == 0) {
            return;
        }
    }
    return this.invokeSuper(isc.ResultTree, "__add", node, parent, position);
},


_preAdd : function (node, parent, removeCollisions, info) {
    var ret = this.invokeSuper(isc.ResultTree, "_preAdd", node, parent, removeCollisions, info);
    if (this.isPaged()) {
        var loadState = this.getLoadState(node),
            newAllCached = (
                loadState === isc.Tree.LOADED ||
                loadState === isc.Tree.LOADED_PARTIAL_CHILDREN);

        this._setVisibleDescendantsCached(node, newAllCached, parent, true);
    }
    return ret;
},


setRoot : function (newRoot, autoOpen) {
    if (this.isPaged()) {

        var newRootFromSameTree = (newRoot && isc.endsWith(this.parentProperty, this.ID));
        this._cleanResultSetChildren(newRootFromSameTree ? newRoot : this.getRoot(), true);
    }
    return this.invokeSuper(isc.ResultTree, "setRoot", newRoot, autoOpen);
},


_preRemove : function (node, parent, info) {
    if (this.isPaged()) {
        this._setVisibleDescendantsCached(node, true, parent, false);
        delete node[this._visibleDescendantsCachedProperty];
        this._cleanResultSetChildren(node, false);
    }
    return this.invokeSuper(isc.ResultTree, "_preRemove", node, parent, info);
},


unloadChildren : function (node, displayNodeType) {
    if (this.isPaged()) {
        this._cleanResultSetChildren(node, false);
    }
    return this.invokeSuper(isc.ResultTree, "unloadChildren", node, displayNodeType);
},

_cleanResultSetChildren : function (node, cleanNonDescendants) {

    var resultSetChildren = this._resultSetChildren;
    for (var i = (resultSetChildren != null ? resultSetChildren.length : 0); i--; ) {
        var children = resultSetChildren[i],
            parentNode = children._parentNode;

        if (cleanNonDescendants ^ (parentNode == node || this.isDescendantOf(parentNode, node))) {

            resultSetChildren.removeAt(i);
            if (this.isObserving(children, "dataArrived")) {
                this.ignore(children, "dataArrived");
            }
            delete children._parentNode;
            delete children._tree;
            delete children.context.keepParentsOnFilter;
            children.setCriteria = isc.ResultSet.getInstanceProperty("setCriteria");
            // Clear the properties from _childrenResultSetProperties.
            delete children._dataAdd;
            delete children._dataAdded;
            delete children._dataRemove;
            delete children._dataRemoved;
            delete children._dataSplice;
            delete children._dataSpliced;
            delete children._dataMoved;
            delete children._dataLengthIsKnownChanged;
        }
    }
},


changeDataVisibility : function (node, newState, callback) {
//!DONTOBFUSCATE  (obfuscation breaks the inline function definitions)

    if (this.isPaged()) {


        var parent = this.getParent(node),
            state = node[this.openProperty],
            changed = (!this.isLeaf(node) && (state ^ newState)),
            closedToOpen = (changed && !state && newState),
            openToClosed = (changed && !closedToOpen),
            visibleCached = node[this._visibleDescendantsCachedProperty],
            parentVisibleCached = (
                parent != null && parent[this._visibleDescendantsCachedProperty]);


        var ret = this.invokeSuper(isc.ResultTree, "changeDataVisibility", node, newState, callback);
        if (parent != null && !visibleCached && (
            (closedToOpen && parentVisibleCached) || (openToClosed && !parentVisibleCached)))
        {
            this._setVisibleDescendantsCached(parent, null, null, false);
        }
        return ret;
    } else {
        return this.invokeSuper(isc.ResultTree, "changeDataVisibility", node, newState, callback);
    }
},

_childrenDataAdd : function (children, parent, addedChildren, addedLength, index, before) {
    var loadingMarker = isc.ResultSet.getLoadingMarker();



    var openSubfoldersAllowed = (
            parent[this.canReturnOpenSubfoldersProperty] != null ?
            parent[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
        defaultChildLength = (
            this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0),
        knownLengthNulls = (
            // The conditions for null/LOADING nodes said to have length 1:
            !(openSubfoldersAllowed || defaultChildLength == 0) &&
            // Only add the 1s from null/LOADING nodes into the length once (so do this only
            // when `!before` is true) and only when the parent's length incorporates the
            // lengths of its children.
            !before &&
            this._includeNodeLengthInParent(null, parent));

    if (addedChildren == null || addedChildren == loadingMarker) {
        if (knownLengthNulls) {
            this._updateParentLengths(parent, addedLength);
        }
        return;
    }
    if (addedLength == 1 && !isc.isAn.Array(addedChildren)) {
        addedChildren = [addedChildren];
    }

    var parentStartRow = this._getStartRow(parent);
    if (parentStartRow === undefined) {

        return;
    }

    if (before) {
        parent[this._recursionCountProperty] = 1 + (parent[this._recursionCountProperty] || 0);
    }

    var infoStack = (this._infoStack = this._infoStack || []),
        collisionsStack = (this._collisionsStack = this._collisionsStack || []),
        collisions,
        grandChildrenStack = [],
        deltaParentLength = 0,
        grandParent,
        origParentLength;
    if (before) {
        collisions = [];
        collisionsStack.push(collisions);
    } else {
        collisions = collisionsStack.pop();
    }


    var i0 = (before ? 0 : addedLength - 1),
        iInc = (before ? 1 : -1);
    for (var i = i0; (before ? i < addedLength : i >= 0); i += iInc) {
        var addedChild = addedChildren[i];
        if (addedChild != null && addedChild != loadingMarker) {
            if (before) {
                var info = {};
                infoStack.push(info);
                var collision = this._findCollision(addedChild);
                if (collision) {
                    collisions.add(collision);
                }
                this._preAdd(addedChild, parent, false, info);
            } else {
                var info = infoStack.pop();
                this._postAdd(addedChild, parent, index, info);
                grandChildrenStack.push(info.grandChildren);


                deltaParentLength += info.deltaParentLength;
                grandParent = info.grandParent;
                origParentLength = info.origParentLength;
            }
        } else if (knownLengthNulls) {
            // The `knownLengthNulls` flag indicates that this null/LOADING is given length 1.
            ++parent[this._cachedLengthProperty];
        }
    }

    if (!before) {



        var fromParent = (parent[this.canReturnOpenSubfoldersProperty] != null),
            openSubfoldersNotAllowed = !openSubfoldersAllowed;

        for (var i = 0; i < addedLength; ++i) {
            var addedChild = addedChildren[i];

            if (addedChild != null && addedChild != loadingMarker) {
                var grandChildren = this._canonicalizeChildren(addedChild, grandChildrenStack.pop(), false);


                if (openSubfoldersNotAllowed &&
                    this.isOpen(addedChild) &&
                    grandChildren != null && !grandChildren.isEmpty())
                {
                    this.logWarn(
                        "Adding the open folder node '" + this.getPath(addedChild) + "' as " +
                        "a child of the parent node '" + this.getPath(parent) + "' is " +
                        "contradictory to the setting of the " + (fromParent ? "'" +
                        this.canReturnOpenSubfoldersProperty + "' property of the parent node." :
                        "'canReturnOpenFolders' property of the tree."));
                }

                if (grandChildren != null) {
                    if (isc.isA.ResultSet(grandChildren)) {

                        if (!(grandChildren.lengthIsKnown() && grandChildren.allMatchingRowsCached())) {
                            this._setVisibleDescendantsCached(addedChild, false, parent, false);
                        }
                    } else if (!isc.isAn.Array(grandChildren)) {
                        this.__add(grandChildren, addedChild);
                    } else {
                        this.__addList(grandChildren, addedChild);
                    }
                }
            }
        }


        if ((--parent[this._recursionCountProperty]) == 0) {
            delete parent[this._recursionCountProperty];
            if (grandParent) {
                deltaParentLength += (this._getNodeLengthToParent(parent, grandParent) - origParentLength);
                this._updateParentLengths(grandParent, deltaParentLength);
            }
        }


        this._setVisibleDescendantsCached(parent, null, null, false);

        if (collisions.length > 0) {
            for (var i = collisions.length; i--; ) {
                var collision = collisions.pop();
                this._removeCollision(collision);
            }
        }
    }
},

_childrenDataRemove : function (children, parent, removedChildren, removedLength, index, before) {
    var loadingMarker = isc.ResultSet.getLoadingMarker();


    // Define conditions for when null/LOADING nodes are given length 1.
    var openSubfoldersAllowed = (
            parent[this.canReturnOpenSubfoldersProperty] != null ?
            parent[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
        defaultChildLength = (
            this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0),
        knownLengthNulls = (
            !(openSubfoldersAllowed || defaultChildLength == 0) &&
            !before &&
            this._includeNodeLengthInParent(null, parent));

    if (removedChildren == null || removedChildren == loadingMarker) {
        if (knownLengthNulls) {
            this._updateParentLengths(parent, -removedLength);
        }
        return;
    }
    if (removedLength == 1 && !isc.isAn.Array(removedChildren)) {
        removedChildren = [removedChildren];
    }

    var parentStartRow = this._getStartRow(parent);
    if (parentStartRow === undefined) {
        return;
    }

    if (before) {
        parent[this._recursionCountProperty] = 1 + (parent[this._recursionCountProperty] || 0);
    }

    var infoStack = (this._infoStack = this._infoStack || []),
        grandParent,
        origParentLength;

    var i0 = (before ? 0 : removedLength - 1),
        iInc = (before ? 1 : -1);
    for (var i = i0; (before ? i < removedLength : i >= 0); i += iInc) {
        var removedChild = removedChildren[i];
        if (removedChild != null && removedChild != loadingMarker) {
            if (before) {
                var info = {};
                infoStack.push(info);
                this._preRemove(removedChild, parent, info);
            } else {
                var info = infoStack.pop();
                this._postRemove(removedChild, parent, info);
                grandParent = info.grandParent;
                origParentLength = info.origParentLength;
            }
        } else if (knownLengthNulls) {
            --parent[this._cachedLengthProperty];
        }
    }

    if (!before) {

        if ((--parent[this._recursionCountProperty]) == 0) {
            delete parent[this._recursionCountProperty];
            if (grandParent) {
                var deltaParentLength = (
                        this._getNodeLengthToParent(parent, grandParent) - origParentLength);
                this._updateParentLengths(grandParent, deltaParentLength);
            }
        }

        this._setVisibleDescendantsCached(parent, null, null, false);
    }
},

_childrenDataSplice : function (children, parent, removedChildren, removedLength, index, addedChildren, addedLength, before) {
    var loadingMarker = isc.ResultSet.getLoadingMarker();


    // Define conditions for when null/LOADING nodes are given length 1.
    var openSubfoldersAllowed = (
            parent[this.canReturnOpenSubfoldersProperty] != null ?
            parent[this.canReturnOpenSubfoldersProperty] : this.canReturnOpenFolders),
        defaultChildLength = (
            this.openDisplayNodeType == isc.Tree.FOLDERS_AND_LEAVES ? 1 : 0),
        knownLengthNulls = (
            !(openSubfoldersAllowed || defaultChildLength == 0) &&
            !before &&
            this._includeNodeLengthInParent(null, parent));

    var removedNodes = !(removedChildren == null || removedChildren == loadingMarker),
        addedNodes = !(addedChildren == null || addedChildren == loadingMarker);

    if (!(removedNodes || addedNodes)) {
        if (knownLengthNulls) {
            this._updateParentLengths(parent, addedLength - removedLength);
        }
        // There is nothing to do.
        return;
    } else if (!addedNodes) {
        if (knownLengthNulls) {
            this._updateParentLengths(parent, addedLength);
        }
        this._childrenDataRemove(children, parent, removedChildren, removedLength, index, before);
        return;
    } else if (!removedNodes) {
        if (knownLengthNulls) {
            this._updateParentLengths(parent, -removedLength);
        }
        this._childrenDataAdd(children, parent, addedChildren, addedLength, index, before);
        return;
    }



    var parentStartRow = this._getStartRow(parent);
    if (parentStartRow === undefined) {
        return;
    }

    if (before) {
        parent[this._recursionCountProperty] = 1 + (parent[this._recursionCountProperty] || 0);
    }

    var infoStack = (this._infoStack = this._infoStack || []),
        collisionsStack = (this._collisionsStack = this._collisionsStack || []),
        collisions,
        grandChildrenStack = [],
        deltaParentLength = 0,
        grandParent,
        origParentLength;
    if (before) {
        collisions = [];
        collisionsStack.push(collisions);
    } else {
        collisions = collisionsStack.pop();
    }

    // ensure addedChildren/removedChildren are arrays as assumed below

    if (addedLength == 1 && !isc.isAn.Array(addedChildren)) {
        addedChildren = [addedChildren];
    }
    if (removedLength == 1 && !isc.isAn.Array(removedChildren)) {
        removedChildren = [removedChildren];
    }

    var i0 = (before ? 0 : addedLength + removedLength - 1),
        iInc = (before ? 1 : -1);
    for (var i = i0; (before ? i < addedLength + removedLength : i >= 0); i += iInc) {

        var add = (removedLength <= i),
            child = (add ? addedChildren[i - removedLength] : removedChildren[i]);
        if (child != null && child != loadingMarker) {
            if (before) {
                var info = {};
                infoStack.push(info);
                if (add) {
                    var collision = this._findCollision(child);
                    if (collision && !(removedNodes && removedChildren.contains(collision))) {
                        collisions.add(collision);
                    }
                    this._preAdd(child, parent, false, info);
                } else {
                    this._preRemove(child, parent, info);
                }
            } else {
                var info = infoStack.pop();
                if (add) {
                    this._postAdd(child, parent, index, info);
                    grandChildrenStack.push(info.grandChildren);


                    deltaParentLength += info.deltaParentLength;
                } else {
                    this._postRemove(child, parent, info);
                }
                grandParent = info.grandParent;
                origParentLength = info.origParentLength;
            }
        } else if (knownLengthNulls) {
            if (add) {
                ++parent[this._cachedLengthProperty];
            } else {
                --parent[this._cachedLengthProperty];
            }
        }
    }

    if (!before) {

        var fromParent = (parent[this.canReturnOpenSubfoldersProperty] != null),
            openSubfoldersNotAllowed = !openSubfoldersAllowed;

        for (var i = 0, j = 0; i < addedLength; ++i) {
            var addedChild = addedChildren[i];
            if (addedChild != null && addedChild != loadingMarker) {
                var grandChildren = this._canonicalizeChildren(addedChild, grandChildrenStack.pop(), false);


                if (openSubfoldersNotAllowed &&
                    this.isOpen(addedChild) &&
                    grandChildren != null && !grandChildren.isEmpty())
                {
                    this.logWarn(
                        "Adding the open folder node '" + this.getPath(addedChild) + "' as " +
                        "a child of the parent node '" + this.getPath(parent) + "' is " +
                        "contradictory to the setting of the " + (fromParent ? "'" +
                        this.canReturnOpenSubfoldersProperty + "' property of the parent node." :
                        "'canReturnOpenFolders' property of the tree."));
                }

                if (grandChildren != null) {
                    if (isc.isA.ResultSet(grandChildren)) {
                        if (!(grandChildren.lengthIsKnown() && grandChildren.allMatchingRowsCached())) {
                            this._setVisibleDescendantsCached(addedChild, false, parent, false);
                        }
                    } else if (!isc.isAn.Array(grandChildren)) {
                        this.__add(grandChildren, addedChild);
                    } else {
                        this.__addList(grandChildren, addedChild);
                    }
                }
            }
        }


        if ((--parent[this._recursionCountProperty]) == 0) {
            delete parent[this._recursionCountProperty];
            if (grandParent) {
                deltaParentLength += (this._getNodeLengthToParent(parent, grandParent) - origParentLength);
                this._updateParentLengths(grandParent, deltaParentLength);
            }
        }


        this._setVisibleDescendantsCached(parent, null, null, false);

        if (collisions.length > 0) {
            for (var i = collisions.length; i--; ) {
                var collision = collisions.pop();
                this._removeCollision(collision);
            }
        }
    }
},

_childrenDataMoved : function (children, parent, movedChildren, movedLength, oldIndex, newIndex) {
    var loadingMarker = isc.ResultSet.getLoadingMarker();

},


_childrenDataLengthIsKnownChanged : function (children, parent, oldValue, newValue) {
    var loadingMarker = isc.ResultSet.getLoadingMarker();



    this._setVisibleDescendantsCached(parent, null, null, false);

},


setDefaultLoadState : function (newDefaultLoadState) {
    var prevDefaultLoadState = this.defaultLoadState;
    this.defaultLoadState = newDefaultLoadState;

    if (this.isPaged() && (
            prevDefaultLoadState !== newDefaultLoadState ||
            (prevDefaultLoadState == null) ^ (newDefaultLoadState == null)))
    {
        var prevFlag = (
                prevDefaultLoadState === isc.Tree.LOADED ||
                prevDefaultLoadState === isc.Tree.LOADED_PARTIAL_CHILDREN),
            newFlag = (
                newDefaultLoadState === isc.Tree.LOADED ||
                newDefaultLoadState === isc.Tree.LOADED_PARTIAL_CHILDREN);

        if (prevFlag ^ newFlag) {

            var root = this.getRoot(),
                nodesWithDefaultLoadState = this.findAll("_loadState", null);

            if (root._loadState == null) {
                if (nodesWithDefaultLoadState == null) {
                    nodesWithDefaultLoadState = [root];
                } else {
                    nodesWithDefaultLoadState.add(root);
                }
            }

            if (nodesWithDefaultLoadState != null) {
                for (var i = nodesWithDefaultLoadState.length; i--; ) {
                    var node = nodesWithDefaultLoadState[i],
                        parent = this.getParent(node);
                    this._setVisibleDescendantsCached(node, null, parent, true);
                }
            }
        }
    }
},


// Criteria / Filtering
// --------------------------------------------------------------------------------------------

//> @method resultTree.setCriteria()
// Set the filter criteria to use when fetching rows.
// <P>
// Depending on the result of +link{compareCriteria()} and setting for
// +link{resultTree.fetchMode}, setting criteria may cause a trip to the server to get a new
// set of nodes, or may simply cause already-fetched nodes to be re-filtered according to the
// new criteria.
// <P>
// For a basic overview on when server fetches are generally performed, see
// +link{resultTree.fetchMode}.
// However, this is not the final determination of when server fetches occur. Criteria can
// be split into local criteria and server criteria by specifying +link{serverFilterFields}.
// Thus, even when using fetchMode:"local" a new server fetch will occur if the server
// criteria changes. For details on how the criteria is split, see
// +link{dataSource.splitCriteria}.
// <P>
// Note: if criteria is being split to retrieve server criteria portion and the criteria
// is an +link{AdvancedCriteria}, the criteria must consist of a single "and" operator
// and one or more simple criteria below it. No other logical operators may be used. In
// other words, the +link{AdvancedCriteria} provided must be exactly representable by a
// simple criteria.
//
// @param newCriteria (Criteria) the filter criteria
// @visibility external
//<
// An overview on tree caching:
// We have 2 kinds of cache
// - the cache of results that matches the current criteria: this.data
// - the cache of the full tree received from the server: this.completeTree
//   * When in local filtering mode both caches are populated. New records from the
//     server always go to completeTree and then the visible tree is filtered from
//     that. Local filtering mode only occurs in fetchMode:"local" with a client-side
//     criteria. Note that if server criteria changes the completeTree cache is
//     overwritten with data from the server and the local cache rebuilt from client-side
//     criteria.
//   * When in basic filtering mode, we only populate the local cache: this.data.
//     On the first fetch we fill this local cache with the results returned from the server.
//     On subsequent changes to filter criteria, we will perform a new server fetch
//     and update the local cache.
setCriteria : function (newCriteria) {
    var oldCriteria = this.criteria || {},
        oldCriteriaIsEmpty = !this.haveCriteria(oldCriteria),
        oldServerCriteria = this._serverCriteria || {},
        ds = this.getDataSource();

    // clone the criteria passed in - avoids potential issues where a criteria object is passed in
    // and then modified outside the RS
    // Avoid this with advanced criteria - our filter builder already clones the output
    if (!ds.isAdvancedCriteria(newCriteria)) {
        // use clone to deep copy so we duplicate dates, arrays etc
        newCriteria = isc.clone(newCriteria);
    }
    this.criteria = newCriteria;

    // If one of the criteria objects is an AdvancedCriteria, convert the other one to
    // enable comparison
    if (ds.isAdvancedCriteria(newCriteria) && !ds.isAdvancedCriteria(oldCriteria)) {
        oldCriteria = isc.DataSource.convertCriteria(oldCriteria);
    }
    if (!ds.isAdvancedCriteria(newCriteria) && ds.isAdvancedCriteria(oldCriteria)) {
        newCriteria = isc.DataSource.convertCriteria(newCriteria);
        this.criteria = newCriteria;
    }

    // serverFilterFields is only applicable for fetchMode:"local" ResultTrees.
    if (this.isLocal() &&
        this.serverFilterFields != null &&
        this.serverFilterFields.length > 0)
    {
        this._localCriteria = isc.DataSource.copyCriteria(newCriteria);
        this._serverCriteria = ds.splitCriteria(this._localCriteria, this.serverFilterFields);
    } else {
        delete this._localCriteria;
        delete this._serverCriteria;
    }

    // See if the criteria changed
    var result = this.compareCriteria(newCriteria, oldCriteria);
    if (result != 0) {
        // Criteria changed

        if (!this.isLocal()) {
            // Not using client-side filtering, just invalidateCache
            //>DEBUG
            this.logInfo("setCriteria: filter criteria changed, invalidating cache");
            //<DEBUG

            this.invalidateCache();

            // Update the criteria on any ResultSet children.
            if (this.isPaged() && this._resultSetChildren != null) {
                var resultSetChildren = this._resultSetChildren.duplicate();
                for (var i = resultSetChildren.length; i--; ) {
                    var children = resultSetChildren[i];
                    if (this._resultSetChildren.contains(children)) {
                        var node = children._parentNode,
                            relationship = this._getRelationship(node, false),
                            childrenCriteria = this._getLoadChildrenCriteria(
                                node, relationship, false);

                        children.setCriteria(
                            isc.DataSource.combineCriteria(childrenCriteria, this.criteria));
                    }
                }
            }
        } else {
            // Filtering locally but the server part of criteria may have changed.
            // If so, we have to invalidate our cache to get a new tree.
            if (this.haveCriteria(this._serverCriteria) ?
                    (this.compareCriteria(this._serverCriteria, oldServerCriteria) != 0) :
                    this.haveCriteria(oldServerCriteria))
            {
                //>DEBUG
                this.logInfo("setCriteria: filter criteria changed, invalidating cache");
                //<DEBUG
                this.invalidateCache();
            } else {
                // No server criteria change
                var openState = this._getOpenState();

                // Make sure we have a complete tree saved if we are starting to filter locally
                if (oldCriteriaIsEmpty) {
                    this.completeTree = this.duplicate(true, true);
                }
                this.filterLocalData();
                // Local filter of existing client side data. No need to check for
                // server-specified open-state.
                this._setOpenState(openState);

                if (this.dataArrived != null) {
                    isc.Func.replaceWithMethod(this, "dataArrived", "parentNode");
                    this.dataArrived(this.getRoot());
                }
            }
        }
    }
},

filterLocalData : function (parentNode) {

    if (!parentNode) parentNode = this.getRoot();

    var criteria = this._localCriteria || this.criteria,
        sourceTree = this.completeTree;

    if (this.haveCriteria(criteria)) {


        // Filter tree
        var filterMode = (this.keepParentsOnFilter ? isc.Tree.KEEP_PARENTS : isc.Tree.STRICT),
            dataSource = this.getDataSource();

        sourceTree = this.applyFilter(this.completeTree, criteria, filterMode,
                         dataSource, this.context);
        //>DEBUG
        this.logInfo("Local filter applied: " + sourceTree.getNodeList().length
                     + " of " + this.completeTree.getNodeList().length
                     + " records matched filter:" + this.echoFull(criteria));
        //<DEBUG
    } else {
        // No criteria anymore. Drop the complete tree as there is no need
        // to keep it updated.
        //>DEBUG
        this.logInfo("Local filter applied: all " + sourceTree.getNodeList().length
                     + " records matched filter: none");
        //<DEBUG
        delete this.completeTree;
    }

    // Remove our existing tree structure (this.data),
    // and instead copy the source tree (filtered or full) into this.data
    // no need to explicitly run _linkNodes() - this will occur as part of
    // setRoot().
    var nodes = sourceTree.getAllNodes();
    nodes = sourceTree.getCleanNodeData(nodes, false, false, true);
    this.data = nodes;

    this.setRoot(this.getCleanNodeData(this.getRoot(), false, false, true));

    this._clearNodeCache(true);
},

//> @method resultTree.applyFilter() [A]
// The ResultTree will call applyFilter() when it needs to locally filter the tree using the
// current filter criteria.
// <P>
// Default behavior is to call +link{tree.getFilteredTree()} to obtain a new, filtered tree.
// <P>
// Override this method or +link{tree.getFilteredTree()} to implement your own client-side
// filtering behavior. Note that the original tree should not be affected.
//
// @param   tree        (Tree)           the source tree to be filtered
// @param   criteria    (Criteria)       the filter criteria
// @param   filterMode  (TreeFilterMode) mode to use for filtering
// @param   dataSource  (DataSource)     dataSource for filtering if the Tree does not
//                                       already have one
// @param [requestProperties] (DSRequest) Request properties block. This allows developers to specify
//  properties that would impact the filter such as +link{DSRequest.textMatchStyle}
// @return  (Tree)     the filtered tree (copy)
// @visibility external
//<
applyFilter : function (tree, criteria, filterMode, dataSource, context) {
    return tree.getFilteredTree(criteria, filterMode, dataSource, context);
},

//> @method resultTree.compareCriteria()
// Default behavior is to call +link{dataSource.compareCriteria()} to determine whether new
// criteria is equivalent to the old criteria (returns 0) or not.
// <P>
// See +link{dataSource.compareCriteria()} for a full explanation of the default behavior.
// The +link{criteriaPolicy} used is "dropOnChange".
// <P>
// Override this method or +link{dataSource.compareCriteria()} to implement your own client-side
// filtering behavior.
//
// @param   newCriteria     (Criteria)  new filter criteria
// @param   oldCriteria     (Criteria)  old filter criteria
// @param   [requestProperties]     (DSRequest Properties)  dataSource request properties
// @return  (Number)    0 if the criteria are equivalent, -1 if the criteria are different
// @see criteriaPolicy
// @visibility external
//<
compareCriteria : function (newCriteria, oldCriteria, requestProperties, policy) {
    return this.getDataSource().compareCriteria(
                newCriteria, oldCriteria,
                (requestProperties ? requestProperties : this.context),
                (policy ? policy : "dropOnChange"));
},

//> @method resultTree.willFetchData()
// Will changing the criteria for this resultTree require fetching new data from the server
// or can the new criteria be satisfied from data already cached on the client?
// <p>
// This method can be used to determine whether +link{TreeGrid.fetchData()} or
// +link{TreeGrid.filterData()} will cause a server side fetch when passed a certain set of
// criteria.
//
// @param newCriteria (Criteria) new criteria to test.
// @return (Boolean) true if server fetch would be required to satisfy new criteria.
// @visibility external
//<
willFetchData : function (newCriteria) {
    var undef;
    var oldCriteria = this.criteria || {},
        oldServerCriteria = this._serverCriteria || {},
        ds = this.getDataSource()
    ;

    // clone the criteria passed in - avoids potential issues where a criteria object is passed in
    // and then modified outside the RS
    // Avoid this with advanced criteria - our filter builder already clones the output
    if (!ds.isAdvancedCriteria(newCriteria)) {
        newCriteria = isc.DataSource.copyCriteria(newCriteria);
    }

    // If one of the criteria objects is an AdvancedCriteria, convert the other one to
    // enable comparison
    if (ds.isAdvancedCriteria(newCriteria) && !ds.isAdvancedCriteria(oldCriteria)) {
        oldCriteria = isc.DataSource.convertCriteria(oldCriteria);
    }
    if (!ds.isAdvancedCriteria(newCriteria) && ds.isAdvancedCriteria(oldCriteria)) {
        newCriteria = isc.DataSource.convertCriteria(newCriteria);
    }

    var result = this.compareCriteria(newCriteria, oldCriteria);

    // If we have no change in criteria we won't perform a fetch
    if (result == 0) return false;

    // If we are not filtering locally a fetch is required

    if (!this.isLocal()) return true;

    // Criteria has changed but we need to see if it affects the fetches. That is the
    // case only if the server criteria portion has changed.

    // Split our criteria to obtain the new server criteria
    var newServerCriteria = {};
    if (this.isLocal() &&
        this.serverFilterFields != null &&
        this.serverFilterFields.length > 0)
    {
        var localCriteria = isc.DataSource.copyCriteria(newCriteria);
        newServerCriteria = ds.splitCriteria(localCriteria, this.serverFilterFields);
    }

    // If server criteria changed, a fetch is required
    return (this.haveCriteria(newServerCriteria) ?
                (this.compareCriteria(newServerCriteria, oldServerCriteria) != 0) :
                this.haveCriteria(oldServerCriteria));
},

// View state
// --------------------------------------------------------------------------------------------


//> @method resultTree.getOpenState()
// Returns a snapshot of the current open state of this tree's data as
// a +link{type:TreeGridOpenState} object.
// <P>
// This object can be passed to +link{resultTree.setOpenState()} or
// +link{treeGrid.setOpenState()} to open the same set of folders
// within the tree's data (assuming the nodes are still present in the data).
// @return (TreeGridOpenState) current open state for the grid.
// @group viewState
// @see resultTree.setOpenState()
// @visibility external
//<
getOpenState : function() {
    var openState = this._getOpenState();
    return isc.Comm.serialize(openState);
},

_getOpenState : function(isCacheRestore) {

    var root = this.getRoot(),
        openState = []
    ;
    this._addNodeToOpenState(this, root, openState, isCacheRestore);

    return openState;
},

//> @method resultTree.setOpenState()
// Reset the set of open folders within this tree's data to match the
// +link{type:TreeGridOpenState} object passed in.
// <P>
// Used to restore previous state retrieved from the tree by a call to
// +link{resultTree.getOpenState()}.
//
// @param openState (TreeGridOpenState) Object describing the desired set of open folders.
// @group viewState
// @see resultTree.getOpenState()
// @visibility external
//<
setOpenState : function(openState) {
    openState = isc.Canvas.evalViewState(openState, "openState", false, this)
    if (!openState) return;
    this._setOpenState(openState);
},

_setOpenState : function(openState, retainServerState) {
    if (!openState) return;
    if (!retainServerState) {
        this.closeAll();
    }
    this.openFolders(openState);
},


_addNodeToOpenState : function (tree, node, openState, isCacheRestore) {
    if (!tree.isOpen(node) || !tree.isLoaded(node)) return false;
    if (isCacheRestore) {
        // explicit call to getName() will set up the "autoAssignedName" flag if this
        // method has never been called before
        tree.getName(node);
        if (this.autoPreserveOpenState == "never" ||
            (this.autoPreserveOpenState == "whenUnique" &&  node._autoAssignedName))
        {
//             this.logWarn("addNodeToOpenState: Skipping node::" + this.echo(node) +
//                 " autoPreserveOpenState:" + this.autoPreserveOpenState);
             return false;
        }
    }

    var children = tree.getFolders(node),
        hasOpenChildren = false;
    if (children != null) {
        for (var i = 0; i < children.length; i++) {
            hasOpenChildren = this._addNodeToOpenState(tree, children[i], openState, isCacheRestore)
                              || hasOpenChildren;
        }
    }
    openState[openState.length] = tree.getPath(node);
    return true;
}

});

// isc._dataModelToString and isc._dataModelLogMessage are defined in Log.js
isc.ResultTree.getPrototype().toString = isc._dataModelToString;
isc.ResultTree.getPrototype().logMessage = isc._dataModelLogMessage;

isc.ResultTree.registerStringMethods({

    //> @method resultTree.dataArrived
    // This callback fires whenever the resultTree receives new nodes from the server, after
    // the new nodes have been integrated into the tree.
    //
    // @param parentNode (TreeNode) The parentNode for which children were just loaded
    //
    // @visibility external
    //<
    dataArrived: "parentNode"
});








isc.Canvas.addMethods({
    // create a DSRequest object in the context of a dataBound component, by gathering
    // various component-level settings that affect how an operation is performed, and
    // creating a dsRequest object (called a "context" for legacy reasons)
    buildRequest : function (context, operationType, callback) {
        if (!context) context = {};

        // "afterFlowCallback": supported by the DataSource system, this is a second callback
        // that fires after dsRequest.callback.  This is a convenience for action methods which
        // need to call a basic dataSource method (eg ds.updateData()) and get called back to
        // do post-processing and *also* need to provide a callback to the end developer.
        // "afterFlowCallback" fires after the callback passed to dataSource methods like
        // addData().
        if (callback) context.afterFlowCallback = callback;

        // text match style:
        // - support the legacy "filter" operationType.
        // - pick up setting on ListGrids and DynamicForms
        if (operationType == "filter") {
            operationType = "fetch";
            // If textMatchStyle was explicitly specified respect it - otherwise mark
            // as a substring match (filter rather than fetch)
            if (context.textMatchStyle == null) context.textMatchStyle = "substring";
        }
        if (this.textMatchStyle != null) context.textMatchStyle = this.textMatchStyle;

        // support old operationType synonyms
        operationType = isc.DS._getStandardOperationType(operationType);

        // pick up component.dataPageSize and component.dataFetchMode for fetches
        if (this.dataPageSize) context.dataPageSize = this.dataPageSize;
        if (this.dataFetchMode) context.dataFetchMode = this.dataFetchMode;

        // pick up an operation name from the component, according to the type of operation
        // being performed.  NOTE: if context.operation is already specified, give it
        // precedence, as this indicates manual invocation with an explicitly specified
        // operation.
        var operation = context.operationId || context.operation;
        if (operation == null) {
            switch (operationType) {
                // NOTE: "saveOperation", "deleteOperation" and the non-specific
                // "this.operation"  are backCompat
                case "fetch":
                    operation = this.fetchOperation; break;
                case "add":
                    operation = this.addOperation || this.saveOperation; break;
                case "update":
                    operation = this.updateOperation || this.saveOperation; break;
                case "remove":
                    operation = this.removeOperation || this.deleteOperation; break;
                case "validate":
                    operation = this.validateOperation; break;
            }
        }
        context.operation = operation || this.operation;

        // If we picked up the operation from component.fetchOperation et al,
        // update the dsRequest operationId as well. This causes it to be displayed in the
        // Log window properly (and means if anyone checks dsRequest.operationId it'll be what
        // they expect to see).
        if (context.operationId == null) {
            context.operationId = context.operation
        }

        // add component ID
        context.componentId = this.ID;
        return isc.rpc.addDefaultOperation(context, this.dataSource, operationType);
    },

    // create a ResultTree dataModel based on the component's current config
    createResultTree : function (criteria, callback, requestProperties, type) {


        if (type == null) type = "fetch";
        this._setupResultTreeFetchCallback(requestProperties, callback);

        if (requestProperties == null) requestProperties = {};
        // put together Tree-specific properties for the data model we're auto-creating
        var tree = isc.addProperties({initialData: this.initialData},
                                     this.dataProperties,
                                     requestProperties.dataProperties
                                     //>!BackCompat 2006.10.19
                                     // Provide back-compat support for 'treeProperties'
                                     ,this.treeProperties,
                                     requestProperties.treeProperties
                                     //<!BackCompat
                                     );
        // If the properties object had a specified ID, drop it.
        // This method can be called repeatedly and we don't want these generated
        // instances clobbering each other

        delete tree.ID;
        tree.criteria = criteria;
        if (requestProperties.operation != null) {
            tree.operation = requestProperties.operation;
        }
        tree.context = requestProperties;
        tree.dataSource = this.dataSource;
        tree.componentId = this.ID;
        // mark as autoCreated so it gets auto-destroyed, removing DS<->RS links
        tree._autoCreated = true;
        // pick up load data on demand for TreeGrids
        if (this.loadDataOnDemand != null) tree.loadDataOnDemand = this.loadDataOnDemand;
        // pick up filter settings for TreeGrids
        if (this.keepParentsOnFilter != null) tree.keepParentsOnFilter = this.keepParentsOnFilter;
        if (this.dataFetchMode != null) tree.fetchMode = this.dataFetchMode;
        if (this.serverFilterFields != null) tree.serverFilterFields = this.serverFilterFields;
        // copy treeRootValue across
        if (this.treeRootValue != null) tree.rootValue = this.treeRootValue;

        // for multi-DS trees: cross-DS relations
        if (this.treeDataRelations) tree.treeRelations = this.treeDataRelations;
        if (this.multiDSTree != null) tree.multiDSTree = this.multiDSTree;

        if (this.progressiveLoading === true || this.progressiveLoading === false) {
            isc.addProperties(tree, { progressiveLoading: this.progressiveLoading });
        }


        var sortSpecifiers = this.getSort();
        if (sortSpecifiers) tree._serverSortBy = isc.DS.getSortBy(sortSpecifiers);

        var resultTreeClass = this.getDataSource().resultTreeClass || "ResultTree";
        return isc.ClassFactory.getClass(resultTreeClass).create(tree);
    },

    _setupResultTreeFetchCallback : function (requestProperties, callback) {
        if ( requestProperties == null ) requestProperties = {};
        // The callback is passed in from fetchData() so should be fired when the server
        // responds with the requested nodes.  Hang it onto the request directly so it fires
        // only when that request returns.

        var context = requestProperties.internalClientContext = {
            _initialFetchCallback: callback
        };

        // By default when filterData is called the callback passed in is stored as the
        // afterFlowCallback for the request.
        // This is the callback we want to fire after initial fetch.
        // If this wasn't passed in explicitly look it up on the request.

        if (requestProperties.afterFlowCallback != null) {
            if (context._initialFetchCallback == null) {
                context._initialFetchCallback = requestProperties.afterFlowCallback;
            } else if (context._initialFetchCallback != requestProperties.afterFlowCallback) {

                this.logWarn("createResultTree called with request.afterFlowCallback:" +
                    this.echo(requestProperties.afterFlowCallback) +
                    " as well as explicit callback parameter " +
                    this.echo(callback) + ". The request.afterFlowCallback will not be fired.");
            }
        }
        requestProperties.afterFlowCallback = {target:this, methodName:"_fireFetchCallback"};
    },

    // callback fired whenever we get new data from the server.
    // use this to fire the callback passed into fetchData if there was one [iff the
    // _isInitialFetch flag is present - set up by ResultTree when performing initial fetch only].
    _fireFetchCallback : function (dsResponse,data,dsRequest) {
        var context = dsRequest.internalClientContext;
        if (context && context._isInitialFetch && context._initialFetchCallback != null) {
            var callback = context._initialFetchCallback;
            this.fireCallback(callback, "dsResponse,data,dsRequest", arguments);
        }
    }
});

//>ValuesManager : add buildRequest to ValuesManager, which isn't a Canvas
if (isc.ValuesManager) {
    isc.ValuesManager.addMethods({
        buildRequest : isc.Canvas.getInstanceProperty("buildRequest")
    });
}
//<ValuesManager

// Many flow methods are identical on databound DynamicForms and ValuesManagers.
// Define these methods in an interface to mix into both classes.

isc.ClassFactory.defineInterface("EditorActionMethods");

isc.EditorActionMethods.addInterfaceMethods({

    // Editing and Saving
    //-------------------------------------------------------------------------------

    //>!BackCompat 2004.7.23
    save : function (context) { return this.saveData(context) },
    editSelected : function (selectionComponent, context) {
        return this.editSelectedData(selectionComponent, context)
    },
    editNew : function (initialValues, context) {
        return this.editNewRecord(initialValues, context)
    },
    //<!BackCompat

    // NOTE: editNewRecord / editRecord / editSelectedData et al do not go to the server,
    // although in a future implementation of editSelected() it may be necessary to go to the
    // server to get fields not being shown in the selection component.

    //>    @method dynamicForm.editNewRecord()
    //
    // Prepare to edit a new record by clearing the current set of values (or replacing them
    // with initialValues if specified).
    // <br>
    // This method will also call +link{DynamicForm.setSaveOperationType()} to ensure
    // subsequent calls to <code>saveData()</code> will use an <code>add</code> rather than
    // an <code>update</code> operation.
    //
    // @param [initialValues] (Object | Record)
    //     initial set of values for the editor as a map of field names to their corresponding
    //     values
    // @see dynamicForm.saveData()
    //
    // @group dataBoundComponentMethods
    // @visibility external
    //<
    editNewRecord : function (initialValues) {
        this.setSaveOperationType("add");
        this._editRecord(initialValues);
    },

    //>    @method dynamicForm.editRecord()
    //
    // Edit an existing record.  Updates this editors values to match the values of the record
    // passed in, via +link{setValues()}.
    // <P>
    // This method will also call +link{DynamicForm.setSaveOperationType()} to ensure
    // subsequent calls to <code>saveData()</code> will use an <code>update</code> rather than
    // an <code>add</code> operation.
    //
    // @param record (Record)
    //     the record to be edited as a map of field names to their corresponding values
    // @see dynamicForm.saveData()
    //
    // @group dataBoundComponentMethods
    // @visibility external
    //<
    editRecord : function (record) {
        var saveOperationType = (record == null ? "add" : "update");
        this.setSaveOperationType(saveOperationType);
        this._editRecord(record);
    },

    _editRecord : function (record) {
        delete this._editRecordNum;
        delete this._editList;
        var record = isc.addProperties({}, record);
        this.setData(record);


        if (this.getFileItemForm != null) {
            var fileItemForm = this.getFileItemForm();
            if (fileItemForm) {
                fileItemForm.editNewRecord();
                fileItemForm.clearErrors(true);
            }
        }
    },

    //>    @method dynamicForm.editSelectedData()
    //
    // Edit the record selected in the specified selection component (typically a
    // +link{ListGrid}).
    // <P>
    // Updates the values of this editor to match the selected record's values.
    // <P>
    // If this form has a dataSource, then saving via +link{saveData()} will use the
    // "update" operation type.
    //
    // @param selectionComponent (ListGrid or ID)
    //     the ListGrid or ID of a +link{ListGrid} whose currently selected
    //     record(s) is/are to be edited
    // @see dynamicForm.saveData()
    //
    // @group dataBoundComponentMethods
    // @visibility external
    // @example updateOperation
    //<
    editSelectedData : function (selectionComponent) {
        // support being passed an ID
        if (isc.isA.String(selectionComponent)) selectionComponent = window[selectionComponent];
        if (!selectionComponent) return;

        var selection = selectionComponent.selection.getSelection();
        // Duplicate and clean the selection before editing
        if (selection && selection.length > 0) {
            var selectionList = [];
            for (var i = 0; i < selection.length; i++) {
                selectionList[i] = selectionComponent.getCleanRecordData(selection[i]);
            }
            this.editList(selectionList);
        }
    },

    editList : function (recordList) {
        this.setSaveOperationType("update");
        this._editRecords(recordList);
    },

    // actually start editing a recordList (start with the first record in the list)
    _editRecords : function (recordList) {
        this._editRecordNum = 0;
        this._editList = recordList;
        var record = isc.addProperties({},recordList[this._editRecordNum]);
        this.editRecord(record);
    },
    editNextRecord : function () {
        this.editOtherRecord(true);
    },

       editPrevRecord : function () {
        this.editOtherRecord(false);
    },

    editOtherRecord : function (next) {
        // not valid if we never had a call to editList.
        if (!this._editList) return;

        if (this.isVisible() && this.valuesHaveChanged()) {
            // remember whether we're editing next or previous
            this._next = next;

            this.saveData({target:this, methodName:"editOtherReply"});
            return;
        };

        if (next && this._editRecordNum >= this._editList.length-1) {
            this.logWarn("Unable to edit next record - this is the last selected record");
            return false;
        }
        if (!next && this._editRecordNum <= 0) {
            this.logWarn("Unable to edit previous record - this is the first selected record");
            return false;
        }
        this._editRecordNum += (next ? 1 : -1);
        var nextRecord = isc.addProperties({}, this._editList[this._editRecordNum]);
        this.setData(nextRecord);
    },
    // reply to the 'save editor' call
    editOtherReply : function (response, data, request) {

        var next = this._next;
        delete this._next;

        // error occurred: the presence of response.errors indicates it's a validation error, which
        // we can handle.  Note we assume the developer meant a validation error if
        // response.errors was provided, regardless of whether you set the correct validation
        // error status code.
        if (response.status < 0 && response.errors) {
            return this.setErrors(response.errors, true);
        }

        // some error we weren't expecting occurred, bail with an error dialog
        if (response.status < 0) return isc.RPCManager._handleError(response, request);

        // remember the values in the form and in the _editlist...
        this.rememberValues();
        this._editList[this._editRecordNum] = this.getValues();

        // call editOther - to show the next record
        this.editOtherRecord(next)
        return true;
    },


    //> @method dynamicForm.validateData()
    //
    // Perform validation on the client and the server.
    //
    // @param [callback] (DSCallback) callback to invoke on completion
    // @param [requestProperties] (DSRequest Properties) additional properties to set on
    //                                                   the DSRequest that will be issued
    //
    // @group validation
    // @visibility external
    //<
    validateData : function (callback, requestProperties) {
        // do simple client side validation first
        if (!this.validate()) return false;
        var values = this.getValues();

        // validate the data on the server
        // Note: As written, if a callback is not supplied we use 'saveDataReply' which will
        // redraw the form to show errors - otherwise we rely on the callback to apply errors
        // to the form.
        var context = this.buildRequest(requestProperties, "validate");
        context.editor = this;

        // valuesAsParams - also sends the DSRequest values as request parameters
        if (context.valuesAsParams) {
            if (!context.params) context.params = {};
            isc.addProperties(context.params, values);
        }

        var dataSource = this.getDataSource();
        return dataSource.validateData(
                   values,
                   callback ? callback : {target:this, methodName:"saveEditorReply"},
                   context
               );
    },

    //>    @method    dynamicForm.reset()   ([])
    //
    // Resets values to the state it was the last time <code>setValues()</code> or
    // <code>rememberValues()</code> was called. If neither of those methods has been called,
    // values will be set back to their initial values at init time.
    //
    // @group formValues
    // @visibility external
    //<
    reset : function () {
        this.resetValues();
    },

    //> @method dynamicForm.cancel() ([])
    //
    // This method exists for clean integration with existing server frameworks that have a 'cancel'
    // feature which typically clears session state associated with the form.  When this method is
    // called, an RPC is sent to the server with a parameter named
    // +link{attr:DynamicForm.cancelParamName} with the value
    // +link{attr:DynamicForm.cancelParamValue}.<p>
    //
    // Note that no other form data is sent.  By default the current top-level page is replaced with the
    // reply.  If you wish to ignore the server reply instead, call this method like this:
    // <pre>
    // dynamicFormInstance.cancel({ignoreTimeout: true, target: null});
    // </pre>
    // @see dynamicForm.cancelEditing()
    // @param [requestProperties] (DSRequest)   additional properties to set on the RPCRequest
    //                                          that will be issued
    // @group submitting
    // @visibility external
    //<
    cancel : function (requestProps) {
        var cancelRPC = {
            actionURL: this.action,
            target: window,
            sendNoQueue: true,
            ignoreTimeout: true,
            useXmlHttpRequest: false,
            params: { }, // set below
            useSimpleHttp: true
        };
        cancelRPC.params[this.cancelParamName] = this.cancelParamValue;

        //>DEBUG
        if (!cancelRPC.actionURL) {
            this.logWarn("No actionURL defined for the cancel RPC - set 'action' on your form or"
                         + "provide an actionURL in the requestProperties to cancel()");
            return;
        }
        //<DEBUG

        isc.addProperties(cancelRPC, requestProps);

        isc.rpc.sendRequest(cancelRPC);
    },

    //> @attr dynamicForm.userTask (UserTask : null : IR)
    // Associated userTask if current dynamic form used along with workflow.
    // See +link{UserTask, userTask} for more details.
    // @visibility workflow
    //<

    //> @method dynamicForm.cancelEditing() ([])
    // If the form or valuesManager has associated userTask workflow task than notify it about
    // cancelling the changes.
    // @visibility external
    //<
    cancelEditing : function () {
        if (this.valuesManager != null) {
            this.valuesManager.cancelEditing();
        }
        if (this.userTask != null) {
            this.userTask.cancelEditing();
        }
    },

    //> @method dynamicForm.completeEditing() ([])
    // Finish editing and store edited values in +link{Process.state,process state}.
    // @visibility external
    //<
    completeEditing : function () {
        if (this.valuesManager != null) {
            this.valuesManager.completeEditing();
        }
        if (this.userTask != null) {
            this.userTask.completeEditing();
        }
    },

    //>    @method    dynamicForm.submit()
    // <code>submit()</code> is automatically called when a +link{SubmitItem} included in the
    // form is clicked, or, if +link{dynamicForm.saveOnEnter,saveOnEnter} is set, when the
    // "Enter" key is pressed in a text input.  Submit can also be manually called.
    // <P>
    // If this form is part of a +link{valuesManager}, this method will simply fall through to
    // the submit method on the valuesManager. If not, and
    // +link{dynamicForm.submitValues(),form.submitValues()} exists, it will be called, and
    // no further action will be taken.
    // <P>
    // Otherwise, default behavior varies based on +link{dynamicForm.canSubmit,form.canSubmit}: if
    // <code>canSubmit</code> is false, +link{method:dynamicForm.saveData()} will be called to
    // handle saving via SmartClient databinding.
    // <P>
    // If <code>canSubmit</code> is true, the form will be submitted like an ordinary HTML
    // form via +link{dynamicForm.submitForm()}.
    // <P>
    // The parameters to <code>submit()</code> apply only if <code>submit()</code> will be
    // calling +link{saveData()}.  If you override <code>submit()</code>, you can safely
    // ignore the parameters as SmartClient framework code does not pass them.
    //
    // @param [callback]          (DSCallback)  callback to invoke on completion.
    //                                          [Ignored if this.canSubmit is true]
    // @param [requestProperties] (DSRequest)   additional properties to set on the DSRequest
    //                                          that will be issued
    //                                          [Ignored if this.canSubmit is true]
    // @group dataBoundComponentMethods
    // @see method:dynamicForm.submitValues()
    // @visibility external
    //<
    submit : function (callback, requestProps) {
    if (this.saveToServer == false) return;

        if (this.valuesManager != null) {
            return this.valuesManager.submit(callback, requestProps);
        }
        if (this.submitValues != null) {
            return this.submitValues(this.getValues(), this);
        }
        if (this.canSubmit) return this.submitForm();
        else return this.saveData(callback, requestProps);
    },

    // returns true if calling saveData() will perform the add operation, false otherwise.
    // Note that the operationType can be overridden by the optional requestProperties arg to
    // saveData.

    saveOperationIsAdd : function () {
        if (this.saveOperationType) return this.saveOperationType == "add";

        if (this.dataSource) {
            var ds = isc.DataSource.getDataSource(this.dataSource);
            return !ds.recordHasAllKeys(this.getValues());
        }
        return false;
    },

    //> @attr dynamicForm.saveToServer (boolean : null : IRWA)
    // Setting <code>saveToServer</code> to false disables the functionality of a
    // +link{method:submit()} call on the form. This is useful when defining a form based on
    // a +link{DataSource} but the normal submit action (typically from a +link{SubmitItem})
    // should be bypassed.
    //<



    //> @attr dynamicForm.suppressValidationErrorCallback (Boolean : false : IRWA)
    // When calling +link{saveData()} on a form or valuesManager, by default if the server
    // returns an error code, any callback passed into saveData() will not be fired.
    // If the error code returned by the server indicates a validation error, it will be
    // displayed to the user by updating the form items to show the error messages, and firing
    // any specified hiddenValidationErrors handler, otherwise the standard RPCManager
    // error handling logic would be invoked.
    // <P>
    // Developers who want to handle errors themselves can override this default by specifying
    // +link{rpcRequest.willHandleError,dsRequest.willHandleError} on the DSRequest. In this case the callback passed in
    // will be fired even if the server returns an error status code.
    // <P>
    // If <code>suppressValidationErrorCallback</code> is set to true, if a save attempt returns
    // a <i>validation</i> error code, the user-specified callback will not be fired
    // <i>even if <code>willHandleError:true</code> was specified on the dsRequest</i>
    // - though for other error codes, the callback would be fired if willHandleError is
    // specified on the request.
    // Note that this is the historical behavior for
    // <smartclient>SmartClient builds 8.0 and earlier</smartclient>
    // <smartgwt>SmartGWT builds 4.0 and earlier</smartgwt>
    // @visibility external
    //<

    //>    @method dynamicForm.saveData()
    //
    // Validate and then save the form's current values to the +link{DataSource} this form is
    // bound to.
    // <p>
    // If client-side validators are defined, they are executed first, and if any errors are
    // found the save is aborted and the form will show the errors.
    // <p>
    // If client-side validation passes, a +link{DSRequest} will be sent, exactly as though
    // +link{dataSource.addData()} or +link{dataSource.updateData()} had been called with
    // +link{dynamicForm.getValues(),the form's values} as data.  The
    // +link{dsRequest.operationType} will be either "update" or "add", depending on the
    // current +link{DynamicForm.saveOperationType}.
    // <P>
    // On either a client-side or server-side validation failure, validation errors will be
    // displayed in the form.  Visible items within a DynamicForm will be redrawn to display
    // errors. Validation failure occurring on hidden items, or DataSource fields with no
    // associated form items may be handled via +link{DynamicForm.handleHiddenValidationErrors}
    // or +link{ValuesManager.handleHiddenValidationErrors}.
    // <P>
    // In the case of a validation error, the callback will <b>not</b> be called by default
    // since the form has already handled the failed save by displaying the validation errors
    // to the user.  If you need to do something additional in this case, you can set
    // +link{rpcRequest.willHandleError} via the <code>requestProperties</code> parameter to
    // force your callback to be invoked.  However, first consider:
    // <ul>
    // <li> if you are trying to customize display of validation errors, there are several
    // +link{dynamicForm.showErrorIcons,built-in modes} and +link{dynamicForm.showErrors()} may be a better
    // place to put customizations.
    // <li> for unrecoverable general errors (eg server is down),
    // +link{RPCManager.handleError,central error handling} in invoked, so consider placing
    // customizations there unless an unrecoverable error should be handled specially by this
    // specific form.
    // </ul>
    //
    // @param [callback]          (DSCallback)  callback to invoke on completion
    // @param [requestProperties] (DSRequest)   additional properties to set on the DSRequest
    //                                            that will be issued
    //
    // @group dataBoundComponentMethods
    // @visibility external
    // @example addOperation
    //<
    // NOTE: not documenting direct submit (If the editor is a multi-part encoded dynamicForm,
    // a direct submit will be performed.)
    saveData : function (callback, requestProperties, noValidation) {

        // If we have a 'selectionComponent', just tell it to pick up the changes and display
        // them
        if (this.selectionComponent != null) {
            var pks = this._selectionComponentRecordPKs;
            // check for 'setRecordValues' is a sanity check only - we should only allow
            // binding to components where this is supported.
            if (pks && this.selectionComponent.setRecordValues) {
                this.selectionComponent.setRecordValues(pks, this.getValues());
            }
            return;
        }

        if (this.dataSource == null) {
            this.logWarn("saveData() called on a non-databound " + this.Class + ". This is not supported. " +
                       " for information on databinding of components look at the documentation" +
                       " for the DataSource class.  " +
                       "If this was intended to be a native HTML form submission, set the " +
                       "canSubmit property to true on this form.");
            return;
        }

        // If both valuesManager and dataPath are set, we don't want to attempt to save this
        // form's data direct - we should just update the ValuesManager
        if (isc.ValuesManager && isc.isA.ValuesManager(this.valuesManager) && this.dataPath) {
            var fields = this.getFields();
            for (var i = 0; i < fields.length; i++) {
                var fieldId;
                if (fields[i].getTrimmedDataPath) {
                    fieldId = fields[i].getTrimmedDataPath();
                }
                if (fieldId == null) {
                    fieldId = fields[i].dataPath || fields[i].name;
                }
                var value = this.getValue(fieldId);
                this.valuesManager._updateValue(fieldId, value, this);
            }
            return;
        }

        if (isc.Offline && isc.Offline.isOffline() && !this.dataSource.clientOnly) {
            isc.warn(this.offlineSaveMessage);
            return;
        }

        //>!BackCompat 2005.3.21 old signature: criteria, requestProperties
        if (requestProperties == null && isc.isAn.Object(callback) &&
            callback.methodName == null)
        {
            // old style call, second param (callback) is really requestParams
            requestProperties = callback;
            callback = requestProperties.afterFlowCallback;
        } //<!BackCompat

        if (requestProperties == null) requestProperties = {};

        var operationType = this.getSaveOperationType(requestProperties);

        // send oldValues to allow long transactions
        // this._oldValues is set up on dynamicForms in rememberValues(), so represents
        // the data when editRecord() or setValues() was called.
        // Assume this is the underlying record object we're editing unless this is
        // an "add" type operation (in which case it was probably passed in as part of
        // editNewRecord(...))

        if (!requestProperties.oldValues) {
            requestProperties.oldValues = operationType != "add" ? this._oldValues : {};
        }

        // do server validation if validationURL is specified
        if (this.validationURL && !noValidation) {
            var validateProps = {};
            isc.addProperties(validateProps, requestProperties);
            isc.addProperties(validateProps, {
                actionURL: this.validationURL,
                valuesAsParams: true,
                sendNoQueue: true
            });
            validateProps._userProps = requestProperties;
            validateProps._userCallback = callback;

            // set a special flag to prevent the validation run called by saveData() from
            // clearing any validation errors currently visible in the form if client-side
            // validation succeeds.  When we're in validationURL mode, typically the server
            // will be supplying these errors and it looks lame when the form redraws between
            // validation submits.
            this.performingServerValidation = true;

            this.validateData(this.getID()+"._saveFormValidateCallback(rpcRequest,rpcResponse,data)",
                              validateProps);
            return;
        }
        // If we're showing a fileItem, we'll have to perform a native commit of the fileItemForm
        // rather than submitting our values in the normal way
        var fileItemForm = this.getFileItemForm();
        // Note: if fileItemForm is undrawn we can't commit it!
        if (fileItemForm && fileItemForm.isDrawn()) {
            this.updateFileItemForm();
            // validators are not duplicated on the fileItem form so explicitly validate now
            if (!this.validate()) return false;

            if (requestProperties.clientContext == null) requestProperties.clientContext = {};
            requestProperties.clientContext.saveDataTarget = this;
            return fileItemForm.saveData(callback, requestProperties, noValidation);
        }

        // hold on to end user callback, and pass our own to the RPC layer.  We do this to
        // provide the formSaved() mechanism that fires before the end user callback.
        this._userCallback = callback;

        callback = this.getID()+"._saveDataReply(dsRequest, dsResponse, data)";
        requestProperties = this.buildRequest(requestProperties, operationType, callback);

        // if the form specified an action different from the default, use it as the RPC target
        var doSubmit = false;

        if (this.submitParamsOnly) requestProperties.useSimpleHttp = true;

        // If this form has an explicitly specified action, submit to the main window rather than
        // to a hidden frame
        if (isc.DynamicForm && isc.isA.DynamicForm(this)) {
            if (this._explicitAction) {
                requestProperties.actionURL = this.action;
                requestProperties.target = this.target ? this.target : window;
                doSubmit = true;
            }

            // override the httpMethod on the request if the user specified a custom 'method'
            if (this.method != isc.DynamicForm.getInstanceProperty("method")) {
                requestProperties.httpMethod = this.method;
            }
        }


        // If the disableValidation flag is set - clear errors before saving - server side validation
        // can still occur and will display new errors if appropriate.
        if (this.disableValidation) this.clearErrors(true);
        else {
            if (!this.validate(null, null, null, null, true)) return false
        }

        if (this.rulesEngine) {
            var rulesValidationResult = this.rulesEngine.processSubmit(this);
            // Rules are technically validators so may fail. If so suppress submission.
            if (rulesValidationResult == false) return;
        }

        // duplicate the values before passing them to save/submit method.
        // This avoids any potential issues where downstream code points to the actual live
        // values object

        var values = this.getDataSource()._cloneValues(this.getValues());

        // perform a direct submit if the form is multipart-encoded

        if ((isc.DynamicForm && isc.isA.DynamicForm(this) && this.isMultipart())
            || this.canSubmit || doSubmit)
        {
            return this.submitEditorValues(values, requestProperties.operation,
                                           requestProperties.callback, requestProperties);
        } else {
            return this.saveEditorValues(values, requestProperties.operation,
                                         requestProperties.callback, requestProperties);
        }

    },

    //> @attr dynamicForm.selectionComponent (DataBoundComponent : null : IRW)
    // May be set to a databound component which displays multiple records and supports
    // selection, such as a ListGrid or TileGrid.<br>
    // If set, the values in this form will be automatically updated on selection change in
    // the selection component to display the (first) selected record's values, and
    // +link{dynamicForm.saveData()} will update the record displayed in the selectionComponent
    // even if no dataSource is specified.
    // @visibility selectionComponent
    //<

    //> @method dynamicForm.setSelectionComponent()
    // setter for +link{dynamicform.selectionComponent}
    // @param component (ID | canvas) new selection component
    //<
    // NOTE: Moved this method up to DataBoundComponent because it is required for ListGrids,
    // DetailViewers, etc.  The partner methods selectionComponentSelectionChanged() and
    // selectionComponentCellSelectionChanged() need DynamicForm-specific implementations,
    // but they had to be moved from here (this is interface EditorActionMethods) and
    // implemented directly on DynamicForm, because interface methods do not override base
    // class impl's and hence we were always picking up the DBC versions of those two.


    // Helper method to prepare the fileItemForm for submission
    updateFileItemForm : function () {
        var fileItemForm = this.getFileItemForm();
        if (fileItemForm == null) return;
        // clear old values and update with our values
        var oldVals = fileItemForm.getValues(),
            vals = this.getValues(),
            uploadField = fileItemForm.getItem(0).getFieldName();
        for (var fieldName in oldVals) {
            if (fieldName == uploadField) continue;
            // set to explicit null rather than just clearValue

            fileItemForm.setValue(fieldName, null);
        }
        for (var fieldName in vals) {
            if (fieldName == uploadField) continue;
            fileItemForm.setValue(fieldName, vals[fieldName]);
        }

        // ensure action and datasource match

        if (this._explicitAction) fileItemForm.setAction(this.action);
        // Note: don't use setDataSource - we don't want to modify the set of fields on the
        // fileItemForm
        fileItemForm.dataSource = this.dataSource;
        fileItemForm.fileItemFormParentForm = this;
    },

    //> @attr dynamicForm.saveOperationType (DSOperationType : null : IRW)
    // Default +link{DSOperationType} to be performed when +link{DynamicForm.saveData()} is called.
    // This property is automatically set on a call to +link{DynamicForm.editRecord()} or
    // +link{DynamicForm.editNewRecord()}, or may be set directly via
    // +link{DynamicForm.setSaveOperationType()}.
    // <P>
    // If <code>saveOperationType</code> is unset, the form will heuristically determine
    // whether an "add" or "update" operation is intended based on whether the primaryKey field
    // is present and editable.
    //
    // @visibility external
    // @getter getSaveOperationType()
    //<

    //> @method dynamicForm.isNewRecord()
    // Returns true if +link{saveOperationType} is currently "add".  See
    // +link{saveOperationType}.
    //
    // @return (Boolean) whether this form will use an "add" operation when saving
    // @visibility external
    //<
    isNewRecord : function () {
        return this.getSaveOperationType() == "add";
    },

    //> @method dynamicForm.setSaveOperationType()
    // Setter for the default +link{DSOperationType} when +link{DynamicForm.saveData()} is called.
    // Note that this property can also be set by calling +link{DynamicForm.editRecord()} or
    // +link{DynamicForm.editNewRecord()}
    //
    // @param operationType (DSOperationType) Operation type to use as a default. Valid values are
    //  <code>"add"</code> or <code>"update"</code>.
    // @visibility external
    //<
    setSaveOperationType : function (operationType) {
        this.saveOperationType = operationType;
    },

    //> @method dynamicForm.getSaveOperationType()
    // Returns the +link{DSOperationType} to be performed when +link{DynamicForm.saveData()} is
    // called. Valid options are <code>"add"</code> or <code>"update"</code>.
    // <P>
    // If a +link{DSRequest} configuration object is passed in containing an explicit operationType
    // this will be returned. Otherwise +link{DynamicForm.saveOperationType} will be returned.
    // This attribute is automatically set via calls to data binding methods such as
    // +link{dynamicForm.editNewRecord}, or it may be set explicitly.
    // <P>
    // If no explicit saveOperationType is specified for this form, the system will
    // look at the current values for the form. If the form has no value for
    // the +link{dataSource.getPrimaryKeyField(),primaryKey field}, or that field is
    // editable and has been modified we assume an add operation, otherwise an update.
    // If the form is a member of a +link{valuesManager}, the primary key field value
    // will be derived from the valuesManager's values object.
    //
    // @param [requestProperties] (DSRequest Properties) Optional DSRequest config block for the
    //  save operation
    // @return (DSOperationType) Operation type for the save request.
    // @visibility external
    //<
    getSaveOperationType : function (requestProperties) {

        var operationType;
        // If no operation was passed in, we're going to have to auto generate one.
        if (!requestProperties || !requestProperties.operation) {
            // Simplify code below to not require null checks
            if (!requestProperties) requestProperties = {};
            // Insert or Update?
            // If the operationType is not passed in, use this.saveOperationType (set by
            // editNew et al)
            operationType = requestProperties.operationType
                             ? requestProperties.operationType
                             : this.saveOperationType;
            // If the saveOperatonType wasn't explicitly provided base it on whether the
            // primary keys for the record are present and whether they've been modified

            if (!operationType && this.dataSource != null) {

                var pkFields = isc.DataSource.getDataSource(this.dataSource).getPrimaryKeyFieldNames(),
                    values = this.getValues(),
                    undef;
                // If we have a valuesManager and we're editing a flat data object, we want
                // to return the operationType if the developer were to call
                // saveData() on the VM, rather than on this form directly.
                if (this.valuesManager && this.dataPath == null) {
                    values = this.valuesManager.getValues();
                }

                for (var i = 0; i < pkFields.length; i++) {
                    var key = pkFields[i],
                        value = values[pkFields];
                    if (value == null) {
                        //this.logWarn('saveData(): has no value for a primary key field:' + key
                        //              + ', assuming this is an add (pk will be genereated by server)');
                        operationType = "add";
                        break;
                    }
                    // checking _oldValues will catch the case where setValues() or
                    // editRecord() [ultimately rememberValues]
                    // was called and passed a value for a field which has subsequently
                    // been modified
                    if (this._oldValues[key] !== undef && this._oldValues[key] != value) {
                        //this.logWarn("saveData(): primary key field:" + key + " has been modified" +
                        //             " assuming this is an add operation");
                        operationType = "add";
                        break;
                    }

                    var item = this.getItem(key);
                    if (item && item.isVisible() && (item.shouldSaveValue && item.isEditable())) {
                        //this.logWarn("saveData(): value for primary key is visible and editable - assuming this is an add");
                        operationType = "add";
                        break;
                    }
                }
                // In this case, every primary key is present and either
                // - doesn't have a form item
                // - the form item is not editable (or not being saved out)
                // So we assume it's unchanged, making this an update of an existing record
                if (operationType == null) {
                    //this.logWarn("saveData(): all primary key fields are present for the record, " +
                    //             "and not editable / edited, so assuming this is an update operation");
                    operationType = "update";
                }
            }
        }


        return operationType;
    },

    // form.saveData() internal callback
    _saveDataReply : function (request, response, data) {
        // If a form contains a FileItem, the request submitted to the server originates from
        // an inner form that wraps the native "upload" component in order to prevent redraws
        // from clearing the upload value.  For this reason, the callback to sync the server's
        // response data will be invoked on this inner form, which is meaningless.  Therefore,
        // intercept this case and hand the callback to the parent form (or valuesManager).
        var clientContext = request.clientContext;
        if (clientContext && clientContext.saveDataTarget != null
            && clientContext.saveDataTarget != this)
        {
            clientContext.saveDataTarget._saveDataReply(request, response, data);
        }

        // this var keeps the index of the next formItem that we need to call formSaved() on.
        this._formSavedIndex = 0;

        // If 'data' is passed back from the server, update this.values with the saved data, except
        // for fields that have subsequently been further updated
        // Exceptions
        // - provide a non obfuscated flag to suppress this data synch
        // - If the server threw an error the data object may be a simple error message

        if (!this.suppressServerDataSync && response && response.status >= 0 && data != null) {
            if (isc.isAn.Array(data)) data = data[0];


            if (request.originalData) request.originalData =isc.shallowClone(request.originalData);
            if (request.data) request.data = isc.shallowClone(request.data);

            // Note: if request.originalData is present, use this rather than request.data
            // This handles the case where request.data may have been reformatted / modified before
            // sending to the server
            // [For example see restDataSource / postMessage dataProtocol where request.data will
            //  be a serialized block of XML]
            // request.originalData matches the values as it was retrieved from the form when
            // the save was kicked off.
            // For iscServer operations use request.data
            // - this object will already be in the "standard" format, and we don't save off
            //   request.originalData in this code-path
            var submittedValues =(request.originalData || request.data),
                currentValues = this.getValues();

            var hasChanges = false,
                rememberValues = true,
                undef;
            // apply per-field changes from submitted to server-saved values to the values object.
            for (var i in data) {
                // If the value for this field is undefined in the submitted data, that probably
                // means it was stripped by the sparseUpdates logic, so we can't compare it to
                // the current value.  However, we can compare it to the corresponding member of
                // _oldValues - the fact that it was stripped by sparseUpdates means that it was
                // unchanged, so if it is different now, it has changed since we sent the update
                // to the server
                var compareVal = submittedValues[i] === undef ? this._oldValues[i] : submittedValues[i];
                var field = this.getField(i);
                // check whether the form item has changed since submission
                if (this.fieldValuesAreEqual(field, currentValues[i], compareVal)) {
                    // if not, check whether the server changed the submitted value to
                    // something else
                    if (!this.fieldValuesAreEqual(field, compareVal, data[i])) {
                        currentValues[i] = data[i];
                        hasChanges = true;
                    }

                } else {
                    // value in the form has changed since being submitted
                    rememberValues = false;
                }
            }
            if (hasChanges) {
                // apply changed field values from data directly to this.values

                this._saveValues(currentValues);
            }
            // Loop through all the items and update them to reflect the changed values.
            // note: we can't just use the attribute names from 'data' - dataPaths applied
            // to items mean we may be reaching into a nested object on the response.
            // We also use this loop to determine whether any changes have been made to items
            // since submission (for fields that weren't present in the submitted values object)
            for (var i = 0; i < this.items.length; i++) {
                var item = this.items[i];
                if (isc.isAn.UploadItem(item)) continue;

                var path = item.dataPath || item.name;
                if (path == null) continue;

                var submittedVal = isc.DynamicForm._getFieldValue(path, item, submittedValues, this, true);

                if (submittedVal === undef ||
                    this.fieldValuesAreEqual(item, submittedVal, item.getValue()))
                {
                    if (hasChanges) {
                        var serverVal = isc.DynamicForm._getFieldValue(path, item, data, this, true);

                        if (!this.fieldValuesAreEqual(item, submittedVal, serverVal)) {
                            item.setValue(serverVal);
                        }
                    }
                } else {
                    rememberValues = false;
                }
            }

            // When the user modifies the values in the form before saving them
            // 'valuesHaveChanged' will return false.
            // Once the save actually completes, if the user hasn't further edited the values,
            // we're effectively editing the (unchanged) record again. At this point
            // re-remember values so valuesHaveChanged returns true.
            if (rememberValues)  {
                this.rememberValues();
            }

            // If this was a save operation, drop the currently specified saveOperationType now
            // if the response included primary key data for the newly added record we're now
            // updating an existing record. We already have logic to catch this case in
            // getSaveOperationType().
            if (this.saveOperationType == "add") delete this.saveOperationType;
        }

        this._callbackState = {
            request: request,
            response: response,
            data: data
        };
        this.formSavedComplete();


        var transactionItem = this.getItem("_transaction");
        if (transactionItem && isc.isA.HiddenItem(transactionItem)) {
            transactionItem.clearValue();
        }

    },

    // notify every FormItem that the form has finished saving.  Used to allow items such as
    // the MultiFileItem to save records which are related a newly created record created by
    // saving of the main form.
    formSavedComplete : function () {

        var fields = this.getFields();
        for (var i = this._formSavedIndex; i < fields.length; i++) {
            this._formSavedIndex++;
            var field = fields[i];
            // call formSaved on the formItem, if defined.  If formSaved() returns false, that
            // means it's going to do some async processing and call this method again when
            // complete.
            if (isc.isA.Function(field.formSaved) &&
                field.formSaved(this._callbackState.request, this._callbackState.response,
                                this._callbackState.data) === false) return;
        }

        // the _userCallback is the original callback specified by the user to saveData().
        // Once we've completed all formSaved() calls, call the user back.
        if (this._userCallback) {
            this.fireCallback(this._userCallback, "dsResponse,data,dsRequest",
                                            [this._callbackState.response,
                                             this._callbackState.data,
                                             this._callbackState.request]);
        }
        delete this._userCallbackState;
        delete this._userCallback;
    },

    // save the given values, displaying any server-side validation errors in the given editor
    saveEditorValues : function (values, saveOperation, callback, context) {
        var undefined;
        if (!context) context = {};

        isc.addProperties(context, {
            prompt:(context.prompt || isc.RPCManager.saveDataPrompt),
            editor:this
        });


        // willHandleError will have to be true so we can show validation errors.
        // However if the user didn't already specify this we need to hang onto the original
        // setting so we can fire default error handling

        context.internalClientContext = {
            _explicitWillHandleError: context.willHandleError
        };
        context.willHandleError = true;


        // valuesAsParams - also sends the DSRequest values as request parameters
        if (context.valuesAsParams) {
            if (!context.params) context.params = {};
            isc.addProperties(context.params, values);
        }

        var dataSource = this.getDataSource();
        return dataSource.performDSOperation(
                    saveOperation.type, values,
                    callback ? callback : {target:this, methodName:"saveEditorReply"}, context);

    },

    // save the given values via direct submit, displaying any server-side validation errors in
    // the given editor
    submitEditorValues : function (values, saveOperation, callback, context) {

        if (!context) context = {};
        isc.addProperties(context, {
            directSubmit : true,
            submitForm : this
        });
        return this.saveEditorValues(values, saveOperation, callback, context);
    },

    // reply to the 'save editor' call
    saveEditorReply : function (response, data, request) {
        if (request.internalClientContext) {
            request.willHandleError = request.internalClientContext._explicitWillHandleError;
        }

        // error occurred: the presence of results.errors indicates it's a validation error,
        // which we can handle.  XXX should really check for status == validation error constant
        if (response.status == isc.RPCResponse.STATUS_VALIDATION_ERROR && response.errors) {
            var saveDataTarget = this;
            var clientContext = request.clientContext;
            if (clientContext && clientContext.saveDataTarget != null) {
                saveDataTarget = clientContext.saveDataTarget;
            }
            saveDataTarget.setErrors(response.errors, true);

            // returning false will avoid the end user callback being called
            return this.suppressValidationErrorCallback ? false :
                    request.willHandleError == true;
        }
        // Standard error handling
        if (response.status < 0 && !request.willHandleError)
            return isc.RPCManager._handleError(response, request);

        // Return true to fire the callback
        return true;
    },

    _saveFormValidateCallback : function (rpcRequest, rpcResponse, data) {
        if (rpcResponse.status == isc.RPCResponse.STATUS_SUCCESS) {
            this.performingServerValidation = false;
            this.markForRedraw("serverValidationSuccess");
            this.saveData(rpcRequest._userCallback, rpcRequest._userProps, true);
            rpcRequest._userCallback = null;
            rpcRequest._userProps = null;
        } else {
            this.setErrors(rpcResponse.errors, true);
        }
    }

});

if (isc.DynamicForm) {
    isc.ClassFactory.mixInInterface("DynamicForm", "EditorActionMethods");

    isc.DynamicForm.addProperties({

        // These overrides moved here from EditorActionMethods because there is a base class
        // implementation that will not be overridden by an interface impl
        selectionComponentSelectionChanged : function (selectionComponent, record, state) {
            if (!state) record = {};  // So the form is cleared when selections are cleared
            this._selectionComponentRecordPKs = selectionComponent.getPrimaryKeys(record);
            this.clearErrors(true);
            if (this.valuesManager && this.valuesManager._setMemberValues) {
                this.valuesManager._setMemberValues(this);
            } else {
                this.editRecord(isc.addProperties({}, record));
            }
        },

        selectionComponentCellSelectionChanged : function (selectionComponent, cellList) {
            for (var i = 0; i < cellList.length; i++) {
                var cell = cellList[i],
                    record = this.selectionComponent.getCellRecord(cell[0], cell[1]);
                if (selectionComponent.cellIsSelected(record)) break;
                record = null;
            }
            if (record) {
                this._selectionComponentRecordPKs = selectionComponent.getPrimaryKeys(record);
                if (this.valuesManager && this.valuesManager._setMemberValues) {
                    this.valuesManager._setMemberValues(this);
                } else {
                    this.editRecord(isc.addProperties({}, record));
                }
            }
        }
    });
}

// Overrides to existing methods on the DF class
isc._EditorFlowOverrides = {
    //>    @method DynamicForm.fetchData()
    // Retrieve data that matches the provided criteria, and edit the first record returned
    //
    // @param [criteria]          (Criteria)      search criteria
    // @param [callback]          (DSCallback)  callback to invoke on completion
    // @param [requestProperties] (DSRequest)   additional properties to set on the DSRequest
    //                                            that will be issued
    //
    // @group dataBoundComponentMethods
    // @visibility external
    //<
    fetchData : function (criteria, callback, requestProperties) {
        var ds = this.getDataSource();
        if (!ds) {
            this.logWarn("Ignoring call to fetchData() on a DynamicForm with no valid dataSource");
            return;
        }
        if (this._fetchDataCallbackArr == null) this._fetchDataCallbackArr = [];
        this._fetchDataCallbackArr.add(callback);

        requestProperties = this.buildRequest(requestProperties, "fetch");

        ds.fetchData(criteria, {target:this, methodName:"fetchDataReply"}, requestProperties);
    },

    fetchDataReply : function (response, data, request) {

        if (data == null || isc.isAn.emptyObject(data) ||
                (isc.isAn.Array(data) && data.getLength() == 0))
        {
            if (response.status == isc.RPCResponse.STATUS_OFFLINE) {
                isc.say(this.offlineMessage);
            }
        }

        var record;
        if (isc.isAn.Array(data)) {
            record = data.get(0);
        } else {
            record = data;
        }
        //var record = data ? data.get(0) : null;
        if (response.status == isc.RPCResponse.STATUS_SUCCESS ||
            response.status == isc.RPCResponse.STATUS_VALIDATION_ERROR)
        {
            this.editRecord(record);
        }
        var callback = this._fetchDataCallbackArr.pop();
        if (callback) this.fireCallback(callback, "dsResponse,data,dsRequest", [response,data,request]);
    },

    //>    @method DynamicForm.filterData()
    // Retrieve data that matches the provided criteria, and edit the first record returned.<br>
    // Differs from +link{DynamicForm.fetchData()} in that a case insensitive substring match
    // will be performed against the criteria to retrieve the data.
    //
    // @param [criteria]          (Criteria)      search criteria
    // @param [callback]          (DSCallback)  callback to invoke on completion
    // @param [requestProperties] (DSRequest)   additional properties to set on the DSRequest
    //                                            that will be issued
    //
    // @group dataBoundComponentMethods
    // @visibility external
    //<
    filterData : function (criteria, callback, requestProperties) {
        var ds = this.getDataSource();
        if (!ds) {
            this.logWarn("Ignoring call to filterData() on a DynamicForm with no valid dataSource");
            return;
        }
        if (this._fetchDataCallbackArr == null) this._fetchDataCallbackArr = [];
        this._fetchDataCallbackArr.add(callback);
        ds.filterData(criteria, {target:this, methodName:"fetchDataReply"}, requestProperties);
    }
}

if (isc.DynamicForm) isc.DynamicForm.addMethods(isc._EditorFlowOverrides);


//>ValuesManager
if (isc.ValuesManager) isc.ClassFactory.mixInInterface("ValuesManager", "EditorActionMethods");

if (isc.ValuesManager) isc.ValuesManager.addMethods(isc._EditorFlowOverrides);

// Pick up fieldValuesAreEqual from the DataBoundComponent methods.

if (isc.ValuesManager) {
    isc.ValuesManager.addProperties({
        // apply the standard fieldValuesAreEqual method to ValueMaps as well as Canvii
        fieldValuesAreEqual:isc.Canvas.getPrototype().fieldValuesAreEqual
    })
}

// Add JSDocs to the ValuesManager as well as the DynamicForm

// NOTE: filterData/clearCriteria are not documented because they are just
// convenience relative to summary.filterData/clearCriteria, and it's confusing/distracting
// for them to have the same names.   However these two methods were exposed briefly in
// 5.1.

//>    @method valuesManager.doExport()
// @include dynamicForm.doExport()
//<

//>    @method valuesManager.editNewRecord()
// @include dynamicForm.editNewRecord()
//<

//>    @method valuesManager.editRecord()
// @include dynamicForm.editRecord()
//<

//> @attr valuesManager.saveOperationType
// @include dynamicForm.saveOperationType
//<

//> @method valuesManager.setSaveOperationType()
// @include dynamicForm.setSaveOperationType()
//<

//> @method valuesManager.isNewRecord()
// @include dynamicForm.isNewRecord()
//<

//> @method valuesManager.getSaveOperationType()
// @include dynamicForm.getSaveOperationType()
//<

//>    @method valuesManager.editSelectedData()
// @include dynamicForm.editSelectedData()
//<

//>    @method valuesManager.saveData()
// @include dynamicForm.saveData()
//<

//> @attr valuesManager.suppressValidationErrorCallback (Boolean : false : IRWA)
// @include dynamicForm.suppressValidationErrorCallback
//<

//>    @method valuesManager.submit()
// <code>submit()</code> is automatically called when a +link{SubmitItem} in a member form
// is clicked, or if +link{dynamicForm.saveOnEnter,saveOnEnter} is set for some member form,
// when the
// "Enter" key is pressed in a text input.  Submit can also be manually called.
// <P>
// If +link{valuesManager.submitValues(),valuesManager.submitValues()} exists, it
// will be called, and no further action will be taken.
// <P>
// Otherwise, +link{method:valuesManager.saveData()} will be called to
// handle saving via SmartClient databinding.
// <P>
// The parameters to <code>submit()</code> apply only if <code>submit()</code> will be
// calling +link{saveData()}.  If you override <code>submit()</code>, you can safely
// ignore the parameters as SmartClient framework code does not pass them.
//
// @include dynamicForm.submit()
// @param [callback]          (DSCallback)  callback to invoke on completion.
// @param [requestProperties] (DSRequest)   additional properties to set on the DSRequest
//                                          that will be issued
// @group dataBoundComponentMethods
// @see method:valuesManager.submitValues()
// @visibility external
//<

//>    @method valuesManager.cancel()
// @include dynamicForm.cancel()
//<

//> @method valuesManager.filterData()
// @include dynamicForm.filterData()
//<

//> @method valuesManager.fetchData()
// @include dynamicForm.fetchData()
//<

//<ValuesManager

if (isc.TreeGrid) {

isc.TreeGrid.addProperties({

    ignoreEmptyCriteria: true
});

isc.TreeGrid.addMethods({


    // TreeGrid.fetchData() / filterData() documented in DataboundComponent.js
    // Overridden to work with ResultTree (Hierarchical data) rather than ResultSet.


    alwaysCreateNewResultTree:false,
    useExistingDataModel : function (criteria, operation, context) {
        if (this.alwaysCreateNewResultTree) return false;
        return this.Super("useExistingDataModel", arguments);
    },

    createDataModel : function (criteria, operation, context) {
        return this.createResultTree(criteria, context.afterFlowCallback, context, null);
    },

    filterWithCriteria : function (criteria, operation, context) {
        var fireSynchronousCallback = false;
        if (this.useExistingDataModel(criteria, operation, context) &&
            isc.ResultTree && isc.isA.ResultTree(this.data))
        {
            // This method will shift the context.afterFlowCallback to the
            // special one-time "initialFetchCallback" slot so it fires once only
            this._setupResultTreeFetchCallback(context);

            delete this.data._performedInitialFetch;
        }
        return this.Super("filterWithCriteria", arguments);
    }

});

}

// DETAIL VIEWING
// --------------------------------------------------------------------------------------------
if (isc.DetailViewer) {

isc.DetailViewer.addMethods({

    //>    @method detailViewer.viewSelectedData()
    //
    // Displays the currently selected record(s) of the selectionComponent widget (typically a
    // listGrid) in the detailViewer.
    //
    // @param selectionComponent (ListGrid or ID)
    //     the ListGrid or ID of a +link{ListGrid} whose currently selected
    //     record(s) is/are to be edited
    //
    // @group dataBoundComponentMethods
    // @visibility external
    //<
    // NOTE: technically, application.viewSelected() has a case where it will issue a DSRequest
    // to fetch the full set of fields, but we don't expose the capability to have a ListGrid
    // load less than the full set of fields right now.
    // @param [callback]         (DSCallback)    callback to invoke on completion
    // @param [requestProperties] (DSRequest)     additional properties to set on the
    //                                             DSRequest that will be issued
    viewSelectedData : function (selectionComponent, callback, requestProperties) {

        // support being passed an ID
        if (isc.isA.String(selectionComponent)) selectionComponent = window[selectionComponent];

        requestProperties = requestProperties || {};

        var selection = selectionComponent.selection.getSelection();
        if (selection && selection.length > 0) {

            // if we're not passed an operation, simply show the records from the selection in the
            // viewer
            if (!requestProperties.operation) {
                this.setData(selection);


            } else {

                // We were passed an operation - perform it to get the record back from the server.
                // This would be required if (for example) the selection components operation is
                // only getting a subset of fields

                // reduce the recordList to just the primary keys
                var operation = requestProperties.operation,
                    dataSource = this.getDataSource(),
                    keys = dataSource.filterPrimaryKeyFields(selection);

                if (requestProperties.prompt == null)
                    requestProperties.prompt = isc.RPCManager.getViewRecordsPrompt;
                requestProperties.viewer = this;

                // actually perform the relevant operation
                return dataSource.performDSOperation(
                                            operation.type, keys,
                                            (callback ? callback : {target:this, methodName:"viewSelectedDataReply"}),
                                            requestProperties
                        );
            }
        }
        return false;
    },

    //>!BackCompat 2004.7.23
    viewSelected : function (selectionComponent, context) {
        return this.viewSelectedData(selectionComponent, context)
    },
    //<!BackCompat

    // handle a reply from the viewSelectedData call
    viewSelectedDataReply : function (response, data, request) {
        this.setData(data);
    }

});

}





// ----------------------------------------------------------------------------------------

isc.defineClass("MockDataSource", "DataSource");

isc.MockDataSource.addClassProperties({

    parseTextWikiSymbols : function (text) {
        var italic = false;
        var bold = false;
        var link = false;
        var res = [];
        for (var i=0; i<text.length; i++) {
            var c = text.charAt(i);
            if (c == '\\') {
                if( (i + 1) < text.length && text.charAt(i + 1) == 'r') {
                    c = "<br/>";
                    i++;
                }
            } else if (c == '[' && text.indexOf("]",i+1) > 0) {
                c = "<a href='#'>";
                link = true;
            } else if (c == ']') {
                if (link) {
                    c = "</a>";
                    link = false;
                }
            } else if (c == '*') {
                if (bold) {
                    bold = false;
                    c = "</b>";
                } else {
                    bold = true;
                    c = "<b>";
                }
            } else if (c == '_') {
                if (italic) {
                    italic = false;
                    c = "</i>";
                } else {
                    italic = true;
                    c = "<i>";
                }
            }
            res.push(c);
        }
        return res.join("");
    },

    parseTableFields : function(tableData, fieldNamingConvention) {
        var rowsData = tableData.split("\n");
        var rawHeaders = rowsData[0].split(","),
             headerArray = []
        ;
        var fieldsParametersLine = rowsData[rowsData.length - 1];
        if (fieldsParametersLine.startsWith("[") &&
            fieldsParametersLine.endsWith("]") &&
            fieldsParametersLine != "[]" &&
            fieldsParametersLine != "[ ]" &&
            fieldsParametersLine != "[x]") {
            var fieldsParameters = fieldsParametersLine.substring(1,
                fieldsParametersLine.length - 1).split(",");
        }
        for (var j = 0; j < rawHeaders.length; j++) {
            var text = rawHeaders[j].trim().replace(/(\\r|\r)/g, "<br/>");
            var name = isc.MockDataSource.convertTitleToName(rawHeaders[j], fieldNamingConvention);
            var actualName = name;
            var iter = 0;
            do {
                var wasSame = false;
                for (var i=0; i < headerArray.length; i++) {
                    if (headerArray[i].name == actualName) {
                        iter++;
                        actualName = name + iter;
                        wasSame = true;
                        break;
                    }
                }
            } while (wasSame);
            if (text == "") {
                text = "&nbsp;";
            }
            var field = {
                name: actualName,
                title: text
            };
            if (field.title.length <= 3) {
                field.align = "center";
            }
            if (fieldsParameters && fieldsParameters[j]) {
                field.width = fieldsParameters[j];
                var lastChar = field.width[field.width.length - 1];
                if (!isc.MockDataSource._isDigit(lastChar)) {
                    field.width = field.width.substring(0, fieldsParameters[j].length - 1);
                    if (lastChar == 'R' || lastChar == 'r') {
                        field.align = "right";
                    } else if (lastChar == 'L' || lastChar == 'l') {
                        field.align = "left";
                    } else if (lastChar == 'C' || lastChar == 'c') {
                        field.align = "center";
                    }
                }
                field.width += "%";
            }
            headerArray.add(field);
        }
        return headerArray;
    },

    convertTitleToName : function (title, fieldNamingConvention) {
        var name = title.trim().replace(/(\\r|\r)/g, "_").replace(/[^a-zA-Z0-9_ ]/g, "_");
        if (name == "" || (name.charAt(0) >= '0' && name.charAt(0) <= '9')) {
            name = '_' + name;
        }
        var parts = name.split(" ");
        name = "";
        // naming conventions used when generating MockDataSource and FormItem field names from
        //  titles:
        //     - "camelCaps" eg "First Name" becomes firstName
        //  - "underscores" eg "First Name" becomes first_name
        //  - "underscoresAllCaps" eg "First Name" becomes FIRST_NAME
        //  - "underscoresKeepCase" eg "First Name" becomes First_Name
        // - default to camelCaps
        if ("underscores" == fieldNamingConvention) {
            for (var i = 0; i < parts.length; i++) {
                name += parts[i].toLowerCase();
                if (i != (parts.length - 1)) {
                    name += "_";
                }
            }
        } else if ("underscoresAllCaps" == fieldNamingConvention) {
            for (var i = 0; i < parts.length; i++) {
                name += parts[i].toUpperCase();
                if (i != (parts.length - 1)) {
                    name += "_";
                }
            }
        } else if ("underscoresKeepCase" == fieldNamingConvention) {
            for (var i = 0; i < parts.length; i++) {
                name += parts[i];
                if (i != (parts.length - 1)) {
                    name += "_";
                }
            }
        } else {
            name = parts[0].toLowerCase();
            // camelCaps is default
            for (var i = 1; i < parts.length; i++) {
                name += parts[i].substring(0, 1).toUpperCase();
                name += parts[i].substring(1).toLowerCase();
            }
        }
        return name;
    },

    // Test for digits
    _isDigit : function(aChar) {
        var myCharCode = aChar.charCodeAt(0);
        if((myCharCode > 47) && (myCharCode <  58)) {
            return true;
        }
        return false;
    },

    parseTable : function(tableData) {
        var lineArray = tableData.split("\n");
        var headerArray = isc.MockDataSource.parseTableFields(tableData);
        var rowArray = [];
        // ignore the first line which is column names
        for (var i=1; i < lineArray.length; i++) {
            if (lineArray[i].startsWith("[") && lineArray[i].endsWith("]") &&
                lineArray[i] != "[]" && lineArray[i] != "[ ]" && lineArray[i] != "[x]") {
                continue;
            }
            var rowObject = {};
            var valueArray = isc.MockDataSource.splitComma(lineArray[i]);
            for (var j=0; j < headerArray.length; j++) {
                var currVal = valueArray[j];
                if (currVal == null) {
                    currVal = "";
                }
                currVal = currVal.replace(/\r/g, "<br/>");
                currVal = currVal.replace("[]", "<input type='checkbox' />");
                currVal = currVal.replace("[ ]", "<input type='checkbox' />");
                currVal = currVal.replace("[x]", "<input type='checkbox' checked='true' />");
                rowObject[headerArray[j].name] = isc.MockDataSource.parseTextWikiSymbols(currVal);
            }
            rowArray.add(rowObject);
        }
        return rowArray;
    },

    splitComma : function(str) {
        var rawParts = str.split(","), parts = [];
        for (var i = 0, len = rawParts.length, part; i < len; ++i) {
            part = "";
            while (rawParts[i].slice(-1) == "\\") {
                part += rawParts[i++].slice(0, -1) + ",";
            }
            parts.push(part + rawParts[i]);
        }
        return parts;
    },

    parseTree : function(treeData) {
        var nodeArray = treeData.split("\n");
        var dataTree = [];
        var lastNode;
        var lastIndent = 0;
        for (var i=0; i < nodeArray.length; i++) {
            var newNode = {};

            var nodeChars = nodeArray[i].split("");
            var indent = 0;
            for (var j=0; j < nodeChars.length; j++) {
                if (nodeChars[j] == " " || nodeChars[j] == ".") {
                    indent += 1;
                } else {
                    break;
                }
            }
            var nodeName = nodeArray[i].substr(indent);

            // detect open folder
            if (nodeName.startsWith("f") || nodeName.startsWith(">")
             || nodeName.startsWith("[x") || nodeName.startsWith("[+")) {
                newNode.isFolder = true;
            // detect closed folder
            } else if (nodeName.startsWith("F") || nodeName.startsWith("v")
             || nodeName.startsWith("[ ") || nodeName.startsWith("[-")) {
                newNode.isFolder = true;
                newNode.isOpen = true;
            }
            var checkedImage = isc.CheckboxItem.getInstanceProperty("checkedImage");
            var uncheckedImage = isc.CheckboxItem.getInstanceProperty("uncheckedImage");
            // set the appropriate icon
            if (nodeName.startsWith("f")) {
            } else if (nodeName.startsWith(">")) {
            } else if (nodeName.startsWith("[x")) {
                newNode.icon = checkedImage;
            } else if (nodeName.startsWith("[+")) {
            } else if (nodeName.startsWith("F")) {
            } else if (nodeName.startsWith("v")) {
            } else if (nodeName.startsWith("[ ")) {
                newNode.icon = uncheckedImage;
            } else if (nodeName.startsWith("[-")) {
            } else if (nodeName.startsWith("-") || nodeName.startsWith("_")) {
                newNode.isFolder = false;
            }
            // strip out node metadata
            if (nodeName.startsWith("[")) nodeName = nodeName.substr(3);
            else if (newNode.isFolder) nodeName = nodeName.substr(1);
            else if (nodeName.startsWith("-") || nodeName.startsWith("_")) nodeName = nodeName.substr(2);

            newNode.name = isc.MockDataSource.parseTextWikiSymbols(nodeName);
            newNode.children = [];
            if (indent == 0) {
                // node is top level
                dataTree.add(newNode);
            } else if (indent > lastIndent) {
                // node is child of previous node
                lastNode.children.add(newNode);
                newNode.parent = lastNode;
            } else if (indent == lastIndent) {
                // node is same level as previous node
                lastNode.parent.children.add(newNode);
                newNode.parent = lastNode.parent;
            } else {
                // indent is less than last indent, so we need to add further up
                // the parent hierarchy
                var ti = lastIndent;
                var parent = lastNode.parent;
                while (ti > indent) {
                    parent = parent.parent;
                    ti -= 1;
                }
                parent.children.add(newNode);
                newNode.parent = parent;
            }
            lastNode = newNode;
            lastIndent = indent;
        }
        return dataTree;
    }
})

isc.MockDataSource.addProperties({
    mockData: "md",
    clientOnly: true,
    cacheData: [],
    fields: [],
    mockDataType: "grid",

    // Override init to setup cacheData and fields using mockData
    init : function () {
        if (this.mockDataType == "grid") {
            this.fields = isc.MockDataSource.parseTableFields(this.mockData);
            this.cacheData = isc.MockDataSource.parseTable(this.mockData);
        } else if(this.mockDataType == "tree") {
            this.fields = [{
                name: "name",
                type: "text"
            }];
            this.cacheData = isc.MockDataSource.parseTree(this.mockData);
        }
        return this.Super("init", arguments);
    }

});


//>    @class DataView
// A DataView coordinates the asynchronous loading of WSDL WebService and XML Schema
// definitions in applications created by Visual Builder.
// <p>
// For applications that do not use WSDL Web Services and were not created by Visual Builder,
// DataView is equivalent to it's superclass +link{VLayout}.
//
// @treeLocation Client Reference/Data Binding
// @visibility external
//<



isc.defineClass("DataView", isc.VLayout).addProperties({
    autoLoadServices:true,
    autoBindDBCs:true
})



isc.Canvas.addProperties({

dataViewInit : function () {

    var operations = this.operations;

    if (operations == null) {
        // no external services to load, bind now
        this.bindToServices();
        return;
    }

    this.operations.setProperty("dataView", this);

    if (!this.autoLoadServices) return;

    // ensure WSDL is loaded for all service operations defined on this dataview (note, relying
    // on duplicate load prevention)
    var locations = operations.getProperty("location").getUniqueItems();

    this.logInfo("loading services: " + locations);

    this._totalWSDLs = locations.length;
    var _this = this;
    for (var i = 0; i < locations.length; i++) {
        isc.xml.loadWSDL(locations[i], function (service) {
            _this._totalWSDLs--;
            _this.logInfo("service loaded: " + service + ", remaining: " + _this._totalWSDLs);
            if (_this._totalWSDLs == 0) {
                _this.dataViewLoaded();
                _this.bindToServices();
            }
        }, null, true); // autoload dependencies
    }
},

// allow additional VMs to be registered
addVM : function (vm) {
    this.addedVMs = this.addedVMs || [];
    this.addedVMs.add(vm);
},

// Maps all DBCs within this DataView to the ValuesManager with the corresponding
// webService/operation/message, if there is one.
bindToServices : function () {

    if (!this.autoBindDBCs) return;


    var dbcs = this.getAllDBCs(this);

    if (!dbcs) return;

    var vms = this.getAllVMs();

    if (this.logIsDebugEnabled("DataView")) {
        this.logDebug("vms: " +
                     this.echoAll(vms.getProperties(["dataSource",
                                                     "serviceNamespace", "serviceName"])) +
                     ", all dbcs: " +
                     this.echoAll(dbcs.getProperties(["dataSource",
                                                      "serviceNamespace", "serviceName"])),
                     "DataView");
    }

    // for each DBC, find the VM it needs to participate in
    for (var i = 0; i < dbcs.length; i++) {
        var dbc = dbcs[i];
        if (dbc.dataSource) {

            if (this.canEdit != null && dbc.canEdit == null) dbc.setCanEdit(this.canEdit);

            var vm = this.findVM(dbc, vms);
            if (vm) {

                if (this.logIsInfoEnabled("dataRegistration")) {
                    this.logWarn("dbc: " + dbc + " binding to dataSource: " + dbc.dataSource +
                                 " and vm: " + vm +
                                 ", with fields: " + this.echoAll(dbc.originalFields),
                                 "dataRegistration");
                }
                if (dbc.originalFields) dbc.setDataSource(dbc.dataSource, dbc.originalFields);
                vm.addMember(dbc);
            } else {
                this.logInfo("no VM for DBC: " + this.echoLeaf(dbc), "DataView");
            }
        }
        // NOTE: items can individually register even if their form is not databound
        if (isc.isA.DynamicForm(dbc) && dbc.items) {
            dbc.items.map("registerWithDataView", this);
        }
    }
},

getAllVMs : function () {
    // collect all the available VMs
    var vms = [];
    var operations = this.operations;
    if (operations) {
        vms.addAll(operations.getProperty("inputVM"));
        vms.addAll(operations.getProperty("outputVM"));
    }
    vms.addAll(this.addedVMs);
    vms.removeList([null]);
    return vms;
},

// find the ValuesManager this DBC should belong to (matching message)
findVM : function (dbc, vms) {
    if (!vms) vms = this.getAllVMs();
    // accept either a DataSource directly or a DBC
    var ds = (isc.isA.DataSource(dbc) ? dbc : dbc.getDataSource());
    for (var i = 0; i < vms.length; i++) {
        var vm = vms[i];
        if (ds == vm.getDataSource()) return vm;
    }
},

// recursively find all DataBoundComponents anywhere under this DataView
getAllDBCs : function (child) {
    var children = child.children;

    if (!children) return null;

    var dbcs = [];
    for (var i = 0; i < children.length; i++) {
        var child = children[i];
        if (isc.isA.DataBoundComponent(child)) dbcs.add(child);
        dbcs.addAll(this.getAllDBCs(child));
    }
    return dbcs;
},

// Registration pattern for inputDataPath
// ---------------------------------------------------------------------------------------
// Refactor: this is almost exactly like what the ValuesManager does on setValues().  It
// may make sense for a component to be allowed to get data from multiple ValuesManager while
// only contributing data to a single ValuesManager.

registerItem : function (item) {
    if (!item.inputDataPath) return;

    var service = isc.WebService.getByName(item.inputServiceName, item.inputServiceNamespace);
    if (!service) {
        this.logWarn("Member: " + item + " could not find webService with name '" +
                     item.inputServiceName + "', " + "namespace '" + item.inputServiceNamespace
                     + "'. Has it been loaded?");
        return;
    }

    var messageID = item.inputSchemaDataSource;

    // store items by message
    var itemRegistry = this.itemRegistry = this.itemRegistry || {};
    var messageItems = itemRegistry[messageID] = itemRegistry[messageID] || [];
    messageItems.add(item);
},

populateListeners : function (vm) {
    var messageID = vm.getDataSource().getID();

    var itemRegistry = this.itemRegistry;

    if (this.logIsInfoEnabled("DataView")) {
        this.logInfo("message: " + messageID +
                     ", registry: " + this.echo(itemRegistry) +
                     ", data: " + this.echo(vm.getValues()), "DataView");
    }

    if (!itemRegistry) return;

    var items = itemRegistry[messageID];
    if (!items) return;

    for (var i = 0; i < items.length; i++) {
        var item = items[i];
        var value = vm.getValue(item.inputDataPath);
        this.logWarn("component: " + item +
                     " with inputDataPath: " + item.inputDataPath +
                     " got data: " + this.echo(value));
        if (isc.isA.FormItem(item)) {
            item.setValue(value);
        } else {
            // update a DataBoundComponent
            item.setData(value);
        }
    }
},

//> @method dataView.dataViewLoaded() (A)
//
// Executed when the dataView has loaded all dependencies (such as DataSources or WebServices).
// No default implementation.
//
// @visibility external
//<

dataViewLoaded : function () {
    // no-op in default impl
}


});

isc.DataView.registerStringMethods({
    dataViewLoaded: ""
});




isc.defineClass("ServiceOperation").addClassMethods({
    getServiceOperation : function (operationName, serviceName, serviceNamespace) {
        if (this._instances) return this._instances.find({
            operationName: operationName,
            serviceName: serviceName,
            serviceNamespace: serviceNamespace
        });
    }
});

isc.ServiceOperation.addMethods({
    init : function () {
        // get a global ID so we can be called in the global scope
        isc.ClassFactory.addGlobalID(this);
        this.Super("init", arguments);
        if (!isc.ServiceOperation._instances) isc.ServiceOperation._instances = [];
        isc.ServiceOperation._instances.add(this);
    },

    invoke : function () {
        var service = this.service = isc.WebService.getByName(this.serviceName, this.serviceNamespace);
        if (!service) {
            this.logWarn("Unable to find web service with serviceName '" + this.serviceName +
                         "' and serviceNamespace '" + this.serviceNamespace + "'. Has it been " +
                         "loaded?");
            return;
        }

        var data = this.inputVM.getValues();
        if (service.useSimplifiedInputs(this.operationName)) {
            data = data[isc.firstKey(data)];
        }

        var _this = this;
        service.callOperation(this.operationName, data, null,
            function (data, xmlDoc, rpcResponse, wsRequest) {
                _this.invocationCallback(data, xmlDoc, rpcResponse, wsRequest);
            }
        );
    },

    invocationCallback : function (data, xmlDoc, rpcResponse, wsRequest) {
        if (!this.outputVM) return;

        // with document style SOAP there is no element representing the message as such - the
        // message parts appear as elements directly.  If there's only a single part there's no
        // representation in the returned data of that single part name (which is a good
        // simplification).  However the dataPaths are specified message-relative and so the
        // partName needs to be explicitly re-introduced
        if (this.service.getSoapStyle(this.operationName) == "document") {
            var fieldNames = this.outputVM.getDataSource().getFieldNames();
            if (fieldNames.length == 1) {
                var firstFieldName = fieldNames.first(),
                    fullData = {};
                fullData[firstFieldName] = data;
                data = fullData;
            }
        }

        this.outputVM.setValues(data);

        if (this.logIsInfoEnabled()) {
            this.logInfo("populating listeners on dataView: " + this.dataView +
                         ", vm has values: " + this.echo(this.outputVM.getValues()));
        }

        // populate inputDataPath listeners via the DataView
        if (this.dataView) this.dataView.populateListeners(this.outputVM);
    }

});



// This file encapsulates SmartClient's HTML5 (and fallbacks) offline support.  For ease of
// porting to pure GWT, isc.Offline is created as a plain JS object with no dependence on
// the SmartClient class system



(function () {

//> @class Offline
// The Offline class provides features for storing and retrieving values in persistent browser
// storage.  Because this storage is both persistent (ie, it remains available after the
// browser is closed and re-opened) and local to the browser, it allows applications to operate
// when the client is not connected to a network, albeit with some obvious limitations.
// <p>
// As well as providing straightforward APIs for storing, retrieving and removing values,
// Offline support is integrated into the normal request/response cycle.  You can configure
// the framework so that server responses are automatically cached in Offline storage.  Then,
// at some future point when the client is offline, responses for known requests are returned
// from the Offline cache.
// <p>
// SmartClient makes use of various underlying storage techniques, depending on what the
// browser supports, but the API to a SmartClient program is the same regardless of the
// underlying storage - simple key/value pairs.  Currently, we provide Offline support for all
// HTML5-compliant browsers, plus earlier versions of Internet Explorer (6 and 7).  The amount
// of storage available is dictated by the browser, and varies from approximately 500KB to
// approximately 5MB.
//
// @example offlineSupport
// @example offlinePrefs
// @treeLocation Client Reference/Data Binding
// @group offlineGroup
// @visibility external
//<

var Offline = {

    // Offline detection and manual override
    // ---------------------------------------------------------------------------------------
    explicitOffline: null,

    //>    @classMethod    Offline.isOffline()
    // Returns true if the current browser session is offline (ie, not connected to a network).
    // If an online/offline state has been set explicitly (see +link{Offline.goOffline()} and
    // +link{Offline.goOnline}), the explicitly-set state will be returned.  Otherwise, the
    // offline state as reported by the browser will be returned.  See
    // +link{useNativeOfflineDetection,useNativeOfflineDetection} for important notes on
    // browser detection of offline state.
    //
    // @return (Boolean) true if the current browser session is offline
    // @see Offline.goOffline()
    // @see Offline.goOnline()
    // @see Offline.useNativeOfflineDetection()
    // @visibility external
    //<
    isOffline : function () {
        // If the explicit offline flag is set (ie, not null), use it in preference to whatever
        // the browser reports
        if (this.explicitOffline !== null) return this.explicitOffline;
        // Working via a local var so it can be flipped in the debugger for ease of testing
        var offline = window.navigator.onLine ? false : true;
        return offline;
    },

    //>    @classMethod    Offline.goOffline()
    // Explicitly sets this session into offline mode.  This setting will override whatever
    // state the browser reports.  This allows users to manually set an application into
    // offline or online state.
    //
    // @see Offline.goOnline()
    // @see Offline.useNativeOfflineDetection()
    // @visibility external
    //<
    goOffline : function () {
        this.explicitOffline = true;
    },

    //>    @classMethod    Offline.goOnline()
    // Explicitly sets this session into online mode.  This setting will override whatever
    // state the browser reports.  This allows users to manually set an application into
    // offline or online state.
    //
    // @see Offline.goOffline()
    // @see Offline.useNativeOfflineDetection()
    // @visibility external
    //<
    goOnline : function () {
        this.explicitOffline = false;
    },

    //>    @classMethod    Offline.useNativeOfflineDetection()
    // Tells the Offline system to query the browser for the current online/offline state.
    // Calling this method switches off the explicit offline mode setting switched on by
    // calling +link{Offline.goOnline()} or +link{Offline.goOffline()}.
    // <p>
    // It is important to note that browsers vary quite considerably in their ability to
    // detect that they are offline.  Many older browsers simply can't do it; HTML5 browsers
    // expose the <code>navigator.onLine</code> property, but each browser's implementation
    // is different.  Some browsers have a manual "Work Offline" mode which allows the user
    // to make the decision, and SmartClient provides an equivalent mechanism with the
    // <code>goOffline</code> and <code>goOnline</code> methods.  Generally speaking, these
    // methods are more reliable than allowing the browser to decide whether your application
    // is offline.
    //
    // @see Offline.goOnline()
    // @see Offline.goOffline()
    // @visibility external
    //<
    useNativeOfflineDetection : function () {
        this.explicitOffline = null;
    },

    // Overall cache managment
    // ---------------------------------------------------------------------------------------
    KEY_PREFIX: "isc-",

    LOCAL_STORAGE: "localStorage",                // HTML5 browsers
    GLOBAL_STORAGE: "globalStorage",              // Older versions of Firefox
    DATABASE_STORAGE: "databaseStorage",          // Safari 3.1 & 3.2 (HTML5 browsers as well,
                                                  // but these older Safari versions have no
                                                  // localStorage support)
    GEARS_DATABASE_API: "gears database api",     // Built into pre-4 versions of Chrome
    USERDATA_PERSISTENCE: "userData persistence", // IE5 and later (IE8+ also support localStorage)
    GOOGLE_GEARS: "google gears",                 // An explicitly installed Gears instance
    NO_MECHANISM: "no discovered mechanism",      // We won't get this very often!

    maxResponsesToPersist: 100,


    userDataPersistenceInIE8: false,

    localStorageType : function () {
        if (window.localStorage) {
            if (!this.userDataPersistenceInIE8 || !isc.Browser.isIE) {
                return this.LOCAL_STORAGE;
            }
        }
        if (window.globalStorage) return this.GLOBAL_STORAGE;
        if (window.openDatabase) return this.DATABASE_STORAGE;
        if (isc.Browser.isIE && isc.Browser.version >= 5) {
            if (!UserDataPersistence.isInitialized) UserDataPersistence.init();
            return this.USERDATA_PERSISTENCE;
        }
        // Not implementing anything else just yet
        return this.NO_MECHANISM;
    },

    // The "native" entry count includes metadata that we store for management and metrics
    getNativeStoredValuesCount : function () {
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                return localStorage.length;
                break;
            case this.USERDATA_PERSISTENCE:
                return UserDataPersistence.getNativeStoredValuesCount();
                break;
            case this.GLOBAL_STORAGE:
            case this.DATABASE_STORAGE:
            case this.GEARS_DATABASE_API:
            case this.GOOGLE_GEARS:
                break;
        }
    },

    // The "SC" entry count is just data entries, gleaned from our own metrics
    getSCStoredValuesCount : function () {
        var entries = this.get(this.countKey);
        entries = entries ? entries * 1 : 0;
        return entries;
    },

    // The "native index" is the index into the underlying browser storage; it is not a stable
    // or predictable value, and thus not a particularly useful one, unless you just want to
    // do an unordered walk of the cache entries.  If you want to iterate over the stored
    // values in a stable, guaranteed sequence, use getPriorityQueueKey/Value/Entry.  This
    // method was only provided to give autoTests a way of verifying that priority queue
    // contents are an exact match for actual stored keys
    getKeyForNativeIndex : function (index) {
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                return localStorage.key(index);
            case this.USERDATA_PERSISTENCE:
                return UserDataPersistence.getKeyForNativeIndex(index);
                break;
            case this.GLOBAL_STORAGE:
            case this.DATABASE_STORAGE:
            case this.GEARS_DATABASE_API:
            case this.GOOGLE_GEARS:
                break;
        }
    },

    clearCache : function () {
        var count = this.getSCStoredValuesCount();
        while (this.getSCStoredValuesCount() > 0) {
            this.removeOldestEntry();
            // This just prevents an eternal loop if for whatever reason running
            // removeOldestEntry() does not result in a different count
            if (this.getSCStoredValuesCount() == count) break;
            count = this.getSCStoredValuesCount();
        }
    },


    clearCacheNatively : function () {
        // userData is nearly always a special case...
        if (this.localStorageType() == this.USERDATA_PERSISTENCE) {
            UserDataPersistence.clearCacheNatively();
            return;
        }
        var count = this.getNativeStoredValuesCount();
        this.logDebug("Removing all " + count + " entries from local storage");
        for (var i = 0; i < count; i++) {
            this._remove(this.getKeyForNativeIndex(0), false);
        }
    },

    logCacheContents : function (maxEntryLength) {
        var contents = this.getCacheContents();
        this.logDebug("Dumping the contents of the browser's local storage:");
        for (var key in contents) {
            var value = contents[key];
            if (value && value.length > maxEntryLength) {
                value = value.substring(0, maxEntryLength);
            }
            this.logDebug(key + " ===> " + value);
        }
    },

    getCacheContents : function () {
        var index = 0,
            contents = {},
            count = this.getSCStoredValuesCount();
        while (index < count) {
            var key = this.getPriorityQueueKey(index);
            // Since we now work via the managed priority queue, we will never get back
            // anything we disn't insert, so this test will always be true
            if (key.indexOf(this.KEY_PREFIX) == 0) key = key.substring(this.KEY_PREFIX.length);
            contents[key] = this.get(key);
            index++;
        }
        return contents;
    },

    getCacheKeys : function () {
        var index = 0,
            keys = [],
            count = this.getSCStoredValuesCount();
        while (index < count) {
            var obj = this.getPriorityQueueKey(index);
            keys[keys.length] = key;
            index++;
        }
        return keys;
    },

    removeOldestEntry : function () {
        var key = this.getAndRemoveOldestFromPriorityQueue();
        if (key == null) return;
        this.remove(key, true);
    },

    // Log the raw contents of userData storage - only intended for internal debug use
    _logUserDataContents : function (maxEntryLength) {
        var doc = this.userDataSpan.xmlDocument;
        var maxEntryLength = 100;
        this.logDebug("Dumping the contents of raw userData storage:");
        for (var i = 0; i < this.userDataSpan.xmlDocument.firstChild.attributes.length; i++) {
            var key = this.userDataSpan.xmlDocument.firstChild.attributes[i].name;
            var value = this.userDataSpan.getAttribute(key);
            if (value && value.length > maxEntryLength) {
                value = value.substring(0, maxEntryLength) + "...";
            }
            this.logDebug(key + " ===> " + value);
        }
    },

    // Managing entries
    // ---------------------------------------------------------------------------------------
    //>    @classMethod    Offline.put()
    // Stores the passed-in value in browser-local storage, mapped to the passed-in key.  If
    // there is no room left to store the value, we discard values from the offline store,
    // oldest first, until there is room to store the value.  If you don't want this behavior,
    // explicitly pass false in the <code>recycleEntries</code> parameter.
    // <p>
    // Note that limitations in the underlying storage engines mean that only primitive
    // values - Strings, numbers and booleans - can be stored.  If you wish to store an Array
    // or Object, you will have to serialize it to JSON first, and then <code>eval</code> it
    // after retrieval to turn it back into an object.
    // <p>
    // <b>Note:</b> This method throws an exception if it could not store the value (either
    // because storage is full and recycleEntries was false, or because the value to store is
    // simply too large)
    //
    // @param key              (String)  The key to use when storing the value
    // @param value            (any)     The value to store
    // @param [recycleEntries] (boolean) If false, suppresses the default behavior of repeatedly
    //                                   discarding the oldest entry if there is insufficient
    //                                   space to store the value
    // @see Offline.get()
    // @visibility external
    //<
    put : function (key, value, recycleEntries) {
        var ts = new Date().getTime();
        var oldValue = this.get(key);
        while (true) {
            try {
                this._put(key, value);
                break;
            } catch (e) {
                if (recycleEntries !== false && this.isStorageException(e)) {
                    var entries = this.getStorageMetrics().storedEntries;
                    if (entries > 0) {
                        this.logDebug("Cache full; removing oldest entry and trying again");
                        this.removeOldestEntry();
                    } else {
                        this.logDebug("Can't add this entry to browser-local storage, even " +
                                     "though the cache is empty - the item must be larger " +
                                     "than the browser's permitted cache space.");
                         break;
                    }
                } else {
                    throw e;
                }
            }
        }
        var end = new Date().getTime();
        var pqOK = false, metricsOK = false;
        while (!pqOK || !metricsOK) {
            try {
                if (!pqOK) this.addToPriorityQueue(key);
                pqOK = true;
                if (!metricsOK) this.updateMetrics("put", key, value, oldValue);
                metricsOK = true;
            } catch(e) {
                if (this.isStorageException(e)) {
                    if (recycleEntries !== false) {
                        var entries = this.getStorageMetrics().storedEntries;
                        if (entries > 0) {
                            this.logDebug("Cache full when updating priority queue or metrics; " +
                                         "removing oldest entry and trying again");
                            this.removeOldestEntry();
                            continue;
                        }
                    }
                    this.logDebug("Cache full when updating priority queue or metrics; rolling " +
                                 "back the entire update");
                    this._remove(key);
                    if (pqOK) this.removeFromPriorityQueue(key);
                    this.rebuildMetrics();
                    throw e;
                } else {
                    throw e;
                }
            }
        }
        this.logDebug("put() with key: " + key + "\nitem: " + this.echoLeaf(value) + ": " +
                        (end - ts) + "ms. Maintaining the priority queue and metrics took " +
                        "a further " + new Date().getTime() - end + "ms");
    },

    _put : function (key, value, applyPrefix) {
        key = (applyPrefix === false ? "" : this.KEY_PREFIX) + key;
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                localStorage.setItem(key, value);
                break;
            case this.USERDATA_PERSISTENCE:
                UserDataPersistence.putValue(key, value);
                break;
            case this.GLOBAL_STORAGE:
            //    globalStorage[''][key] = value;
            //    break;
            case this.DATABASE_STORAGE:
            //    this.database.executeSql(
            //        "INSERT INTO ResponseCache (key, value) " +
            //        "('" + key + "', '" + value + "')");
            //    break;
            case this.GEARS_DATABASE_API:
            case this.GOOGLE_GEARS:
                this.logError("Persistence method '" + this.localStorageType() + "' not yet supported");
                break;
        }
    },

    isStorageException : function (e) {
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                if (isc.Browser.isIE) {
                    return (e.number == -2147024882);
                } else if (isc.Browser.isMoz) {
                    return (e.name == "NS_ERROR_DOM_QUOTA_REACHED");
                } else {
                    return (e.name == "QUOTA_EXCEEDED_ERR");
                }
                break;
            case this.USERDATA_PERSISTENCE:
                return (e.number == -2147024857)
        }
    },

    //>    @classMethod    Offline.get()
    // Returns the value mapped to the passed-in key from browser-local storage, or null if
    // no such key exists.
    //
    // @param key              (String)  The key to retrieve a value for
    // @return The value mapped to the passed in key, or null if no such key exists
    // @see Offline.put()
    // @visibility external
    //<
    get : function (key) {
        var ts = new Date().getTime(),
            item;
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                item = localStorage.getItem(this.KEY_PREFIX + key);
                break;
            case this.USERDATA_PERSISTENCE:
                item = UserDataPersistence.getValue(this.KEY_PREFIX + key);
                break;
            case this.GLOBAL_STORAGE:
            //    return globalStorage[''][key];
            case this.DATABASE_STORAGE:
                // Not sure what's involved yet
            case this.GEARS_DATABASE_API:
            case this.GOOGLE_GEARS:
                break;
        }

        if (item) item = isc.clone(item);

        var end = new Date().getTime();
        this.logDebug("get() with key: " + key +
                     "\nitem is: " + this.echoLeaf(item) +
                     ": " + (end - ts) + "ms");
        return item;
    },


    //>    @classMethod    Offline.remove()
    // Removes the key/value pair mapped by the passed-in key from browser-local storage
    //
    // @param key              (String)  The key to remove
    // @see Offline.put()
    // @see Offline.get()
    // @visibility external
    //<
    remove : function (key, skipPriorityQueueUpdate) {
        this.logDebug("Removing item for key: " + key);
        this.updateMetrics("remove", key);
        if (!skipPriorityQueueUpdate) this.removeFromPriorityQueue(key);
        this._remove(key);
    },

    _remove : function (key, applyPrefix) {
        key = (applyPrefix === false ? "" : this.KEY_PREFIX) + key;
        switch (this.localStorageType()) {
            case this.LOCAL_STORAGE:
                localStorage.removeItem(key);
                break;
            case this.USERDATA_PERSISTENCE:
                UserDataPersistence.removeValue(key);
                break;
            case this.GLOBAL_STORAGE:
            //    return globalStorage[''][key];
            case this.DATABASE_STORAGE:
                // Not sure what's involved yet
            case this.GEARS_DATABASE_API:
            case this.GOOGLE_GEARS:
                break;
        }
    },

    getUndecoratedKey : function (key) {
        if (key && key.startsWith(this.KEY_PREFIX)) {
            key = key.substring(this.KEY_PREFIX.length);
        }
        return key
    },


    // ---------------------------------------------------------------------------------------

    priorityQueueKey: "pq",  // this will actually have "isc-" prepended to it
    addToPriorityQueue : function (userKey) {
        //!DONTOBFUSCATE
        this.removeFromPriorityQueue(userKey);
        var key = this.toInternalKey(userKey);
        var pqText = this.get(this.priorityQueueKey);
        if (pqText) {
            eval("var pq = " + pqText);
        } else {
            var pq = [];
        }
        pq.push(key);
        this._put(this.priorityQueueKey, this.serialize(pq));
    },
    removeFromPriorityQueue : function (userKey) {
        //!DONTOBFUSCATE
        var key = this.toInternalKey(userKey);
        var pqText = this.get(this.priorityQueueKey);
        if (pqText) {
            eval("var pq = " + pqText);
        } else {
            var pq = [];
        }
        for (var i = 0; i < pq.length; i++) {
            if (pq[i] == key) {
                var leading = pq.slice(0, i);
                var trailing = pq.slice(i + 1);
                pq = leading.concat(trailing);
                break;
            }
        }
        this._put(this.priorityQueueKey, this.serialize(pq));
    },
    getAndRemoveOldestFromPriorityQueue : function () {
        //!DONTOBFUSCATE
        var pqText = this.get(this.priorityQueueKey);
        if (pqText) {
            eval("var pq = " + pqText);
        } else {
            var pq = [];
        }
        var oldest = pq.shift();
        this._put(this.priorityQueueKey, this.serialize(pq));
        return this.toUserKey(oldest);
    },
    getPriorityQueueEntry : function (index) {
        var key = this.getPriorityQueueKey(index);
        var value = this.get(key);
        var entry = {};
        entry[key] = value;
        return entry;
    },
    getPriorityQueueValue : function (index) {
        var key = this.getPriorityQueueKey(index);
        return this.get(key);
    },
    getPriorityQueueKey : function (index) {
        //!DONTOBFUSCATE
        var pqText = this.get(this.priorityQueueKey);
        if (pqText) {
            eval("var pq = " + pqText);
        } else {
            var pq = [];
        }
        return this.toUserKey(pq[index]);
    },
    getPriorityQueue : function () {
        //!DONTOBFUSCATE
        var pqText = this.get(this.priorityQueueKey);
        if (pqText) {
            eval("var pq = " + pqText);
        } else {
            var pq = [];
        }
        return pq;
    },

    toInternalKey : function (userKey) {
        if (this.localStorageType() == this.USERDATA_PERSISTENCE) {
            return UserDataPersistence.getDataStoreKey(this.KEY_PREFIX + userKey);
        }
        // Otherwise, we don't mess about with converting keys
        return userKey;
    },

    toUserKey : function (internalKey) {
        if (this.localStorageType() == this.USERDATA_PERSISTENCE) {
            return this.getUndecoratedKey(UserDataPersistence.getUserKey(internalKey));
        }
        // Otherwise, we don't mess about with converting keys
        return internalKey;
    },

    // Storage metrics
    // ---------------------------------------------------------------------------------------
    countKey: "storedEntryCount__",
    keyKey: "storedKeyBytes__",
    valueKey: "storedValueBytes__",

    updateMetrics : function (mode, key, value, oldValue) {
        var realKey = this.KEY_PREFIX + key,
            storedEntries = this.get(this.countKey) || 0,
            storedKeyBytes = this.get(this.keyKey) || 0,
            storedValueBytes = this.get(this.valueKey) || 0;
            storedKeyBytes = 1 * storedKeyBytes;
            storedValueBytes = 1 * storedValueBytes;
            if (mode == "remove") {
                var item = this.get(key);
                storedEntries--;
                storedKeyBytes -= realKey.length;
                storedValueBytes -= item.length;
            } else {
                if (oldValue == null) {
                    storedEntries++;
                    storedKeyBytes += realKey.length;
                    storedValueBytes += value.length;
                } else {
                    storedValueBytes += value.length - oldValue.length;
                }
            }
            this._put(this.countKey, storedEntries);
            this._put(this.keyKey, storedKeyBytes);
            this._put(this.valueKey, storedValueBytes);
    },

    rebuildMetrics : function () {
        var pq = this.getPriorityQueue(),
            entries = 0, keyBytes = 0, valueBytes = 0;
        for (var i = 0; i < pq.length; i++) {
            var value = this.get(pq[i]);
            entries++;
            keyBytes += pq[i].length;
            valueBytes += value.length;
        }
        this._put(this.countKey, entries);
        this._put(this.keyKey, keyBytes);
        this._put(this.valueKey, valueBytes);
    },

    getStorageMetrics : function () {
        var storedEntries = this.get(this.countKey) || 0,
            storedKeyBytes = this.get(this.keyKey) || 0,
            storedValueBytes = this.get(this.valueKey) || 0,
            countLen = 0,
            keyLen = 0,
            valueLen = 0;
        if (storedEntries) countLen = storedEntries.length;
        if (storedKeyBytes) keyLen = storedKeyBytes.length;
        if (storedValueBytes) valueLen = storedValueBytes.length;
        storedEntries = 1 * storedEntries;
        storedKeyBytes = 1 * storedKeyBytes;
        storedValueBytes = 1 * storedValueBytes;
        var pqText = this.get(this.priorityQueueKey);
        var overhead = this.countKey.length + this.keyKey.length + this.valueKey.length +
                             countLen + keyLen + valueLen;
        var pqLength = pqText == null ? 0 : pqText.length +
                                (this.KEY_PREFIX + this.priorityQueueKey).length;
        return {
            storedEntries: storedEntries,
            storedKeyBytes: storedKeyBytes,
            storedValueBytes: storedValueBytes,
            metricsOverhead: overhead,
            priorityQueue: pqLength,
            total: storedKeyBytes + storedValueBytes + overhead + pqLength
        }
    },

    getTotalStorageUsed : function () {
        var metrics = this.getStorageMetrics();
        return metrics.storedKeyBytes +
               metrics.storedValueBytes +
               metrics.metricsOverhead +
               metrics.priorityQueue;
    },

    // DataSource functionality
    // ---------------------------------------------------------------------------------------
    storeResponse : function (dsRequest, dsResponse) {
        var ts = new Date().getTime();
        dsResponse.offlineTimestamp = ts;

        var trimmedRequest = this.trimRequest(dsRequest),
            key = this.serialize(trimmedRequest),
            value = this.serialize(this.trimResponse(dsResponse));

        this.logDebug("storeResponse serializing: " + (new Date().getTime() - ts) + "ms");

        // Unless we are already storing a response for this request (in which case we'll just
        // freshen it), check if we're about to bust the maximum responses limit
        if (this.get(key) == null) {
            if (this.getSCStoredValuesCount() >= this.maxResponsesToPersist) {
                this.removeOldestEntry();
            }
        }
        this.put(key, value);
    },

    trimRequest : function (dsRequest) {
        var keyProps = ["dataSource", "operationType", "operationId",
                        "textMatchStyle", "values", "sortBy",
                        "startRow", "endRow", "data"],

            trimmed = {},
            undef;
        for (var i = 0; i < keyProps.length; i++) {
            if (dsRequest[keyProps[i]] !== undef) {
                trimmed[keyProps[i]] = dsRequest[keyProps[i]];
            }
        }

        return trimmed;
    },
    trimResponse : function (dsResponse) {
        var keyProps = ["dataSource",
                        "startRow", "endRow", "totalRows",
                        "data", "offlineTimestamp",
                        "status", "errors", // note we don't actually cache errors yet
                        "invalidateCache", "cacheTimestamp"],
            trimmed = {},
            undef;
        for (var i = 0; i < keyProps.length; i++) {
            if (dsResponse[keyProps[i]] !== undef) {
                trimmed[keyProps[i]] = dsResponse[keyProps[i]];
            }
        }
        return trimmed;
    },

    getResponse : function (dsRequest) {
        //!DONTOBFUSCATE
        var trimmedRequest = this.trimRequest(dsRequest),
            key = this.serialize(trimmedRequest),
            value = this.get(key),
            returnValue;
        // using raw eval() because SmartClient might not be available
        eval('returnValue = ' + value);
        if (returnValue) returnValue.fromOfflineCache = true;
        return returnValue;
    },
    serialize : function (obj) {
        return isc.Comm.serialize(obj, false);
    },

    // Offline storage browser
    // ---------------------------------------------------------------------------------------
    showStorageInfo : function () {
        if (!this.storageBrowser) {
            if (isc.Offline.localStorageType() == isc.Offline.USERDATA_PERSISTENCE) {
                isc.Timer.setTimeout(function () {
                isc.say("WARNING:  This browser uses an old storage mechanism that does not " +
                        "permit arbitrary key/value pair storage.  This means we have to " +
                        "store extra management data, with the upshot that the metrics reported " +
                        "for 'priority queue' and 'overhead' are indicative, but not accurate");
                }, 0);
            }
            this.metricsDF = isc.DynamicForm.create({
                width: "100%",
                numCols: 6,
                fields: [
                    {name: "storedEntries", title: "No. entries", disabled: true},
                    {name: "storedKeyBytes", title: "Used by keys", disabled: true},
                    {name: "storedValueBytes", title: "Used by values", disabled: true},
                    {name: "priorityQueue", title: "Used by Priority Queue", disabled: true},
                    {name: "metricsOverhead", title: "Metrics overhead", disabled: true},
                    {name: "total", title: "Total Bytes", disabled: true}
                ]
            });
            this.storageLG = isc.ListGrid.create({
                width: "100%",
                height: "*",
                canRemoveRecords: true,
                removeData: function (record) {
                    isc.ask("Remove this entry?", function (value) {
                        if (value) {
                            isc.Offline.remove(record.key);
                            isc.Offline.refreshStorageInfo();
                        }
                    });
                },
                rowDoubleClick: function(record) {
                    isc.Offline.createStorageEditorWindow();
                    isc.Offline.storageEditorWindow.show();
                    isc.Offline.storageEditor.editRecord(record);
                },
                fields: [
                    {name: "key", width: "25%", title: "Key"},
                    {name: "value", title: "Value"}
                ]
            });
            this.storageBrowser = isc.Window.create({
                autoCenter: true,
                canDragResize: true,
                width: Math.floor(isc.Page.getWidth() * 0.5),
                height: Math.floor(isc.Page.getHeight() * 0.5),
                title: "Offline Storage",
                items: [
                    this.metricsDF,
                    this.storageLG,
                    isc.HLayout.create({
                        width: "100%", height: 1,
                        members: [
                            isc.LayoutSpacer.create({width: "*"}),
                            isc.Button.create({
                                title: "Add Entry",
                                click: function () {
                                    isc.Offline.createStorageEditorWindow();
                                    isc.Offline.storageEditorWindow.show();
                                    isc.Offline.storageEditor.editNewRecord();
                                }
                            })
                        ]
                    })
                ]
            });
        }

        this.storageBrowser.show();
        this.refreshStorageInfo();
    },

    createStorageEditorWindow : function () {
        if (!isc.Offline.storageEditorWindow) {
            isc.Offline.storageEditor = isc.DynamicForm.create({
                fields: [
                    {name: "key", title: "Key", editorType: "TextAreaItem", width: 400},
                    {name: "value", title: "Value", editorType: "TextAreaItem", width: 400},
                    {name: "saveButton", type: "button", title: "Save", click: function () {
                        var form = isc.Offline.storageEditor;
                        if (form.saveOperationType == "update" &&
                                form.getValue("key") != form.getOldValue("key"))
                        {
                            isc.ask("Key has changed - this will create a new entry. " +
                                    "Do you want to retain the old entry as well? (if " +
                                    "you answer 'No', it will be removed",
                                    function (value) {
                                        if (value === false) {
                                            isc.Offline.remove(form.getOldValue("key"));
                                        }
                                        if (value != null) {
                                            isc.Offline.put(form.getValue("key"),
                                                            form.getValue("value"));
                                            isc.Offline.storageEditorWindow.hide();
                                            isc.Offline.refreshStorageInfo();
                                        }
                                    });
                        } else {
                            isc.Offline.put(form.getValue("key"), form.getValue("value"));
                            isc.Offline.storageEditorWindow.hide();
                            isc.Offline.refreshStorageInfo();
                        }
                    }}
                ]
            });
            isc.Offline.storageEditorWindow = isc.Window.create({
                bodyProperties: { margin: 5 },
                title: "Edit Offline Storage Entry",
                isModal: true,
                autoCenter: true,
                height: 280,
                width: 480,
                items: [
                    isc.Offline.storageEditor
                ]
            });
        }
    },

    refreshStorageInfo : function () {
        this.metricsDF.editRecord(isc.Offline.getStorageMetrics());
        var dataObj = isc.Offline.getCacheContents();
        var data = [];
        for (var key in dataObj) {
            data.add({key: key, value: dataObj[key]});
        }
        this.storageLG.setData(data);
    }

};

var UserDataPersistence = {

    isInitialized: false,
    poolSize: 10,

    keyIndexKey: "keyIndex",
    reverseKeyIndexKey: "reverseKeyIndex",


    init : function () {
        this.userDataSpan = [];
        for (var i = 0; i < this.poolSize; i++) {
            this.userDataSpan[i] = document.createElement('span');
            this.userDataSpan[i].ID = 'isc_userData_' + i;
            this.userDataSpan[i].style.behavior = 'url(#default#userdata)';
            document.body.appendChild(this.userDataSpan[i]);
            this.userDataSpan[i].load("isc_userData_" + i);
        }
        this.keyIndexStore = document.createElement('span');
        this.keyIndexStore.ID = 'isc_userData_keyIndex';
        this.keyIndexStore.style.behavior = 'url(#default#userdata)';
        document.body.appendChild(this.keyIndexStore);
        this.keyIndexStore.load("isc_userData_keyIndex");

        this.keyIndex = this.getKeyIndexFromStore();
        this.reverseKeyIndex = this.getReverseKeyIndexFromStore();
        if (!this.keyIndex) {
            this.keyIndex = {};
            this.reverseKeyIndex = {};
        } else if (!this.reverseKeyIndex) this.buildReverseKeyIndex();

        this.buildNextAttributeInfo();

        this.isInitialized = true;
    },

    clearCacheNatively : function () {
        for (var i = 0; i < this.poolSize; i++) {
            var attrs = this.userDataSpan[i].xmlDocument.firstChild.attributes;
            while (attrs.length > 0) {
                this.userDataSpan[i].removeAttribute(attrs[0].name);
            }
        }
        this.keyIndex = {};
        this.reverseKeyIndex = {};
        this.saveKeyIndex();
    },

    getNativeStoredValuesCount : function () {
        var count = 0;
        for (var i = 0; i < this.poolSize; i++) {
            count += this.userDataSpan[i].xmlDocument.firstChild.attributes.length;
        }
        return count;
    },


    getKeyForNativeIndex : function (index) {
        var iCounter = 0;
        for (var i = 0; i < this.poolSize; i++) {
            if (iCounter + this.userDataSpan[i].xmlDocument.firstChild.attributes.length > index) {
                var offsetIndex = index - iCounter;
                var attrName = this.userDataSpan[i].xmlDocument.firstChild.attributes[offsetIndex].name,
                    attrNum = attrName.substring(1),
                    dsKey = this.getKeyIndexValue(i, attrNum);
                return this.getUserKey(dsKey);
            }

        }
    },

    getKeyIndexValue : function (index, attrName) {
        var attrNum = attrName.substring(1);
        if (index == 0) {
            return "00000".substring(attrNum.length) + attrNum;
        }
        return index * 10000 + (1 * attrNum);
    },

    getUserKey : function (userKey) {
        return this.reverseKeyIndex[userKey];
    },

    getDataStoreKey : function (key) {
        return this.keyIndex[key];
    },

    _getValue : function (dataStore, attr) {
        return this.userDataSpan[dataStore].getAttribute(attr);
    },

    getValue : function (userKey) {
        var key = this.getDataStoreKey(userKey),
            undef;
        if (key === undef) return null;
        var dataStore = ("" + key).substring(0, 1),
            attr = "v" + (("" + key).substring(1) * 1);
        return this._getValue(dataStore, attr);
    },


    putValue : function (userKey, value) {
        var key = this.getDataStoreKey(userKey);
        if (key) {  // We're already storing a value for this key
            var dataStore = ("" + key).substring(0, 1),
                attr = "v" + (("" + key).substring(1) * 1),
                savedValue = this._getValue(dataStore, attr);
        } else {
            var dataStore = this.getDataStoreForNewItem(),
                attr = this.getNextAttributeName(dataStore);
        }
        this.userDataSpan[dataStore].setAttribute(attr, value);


        try {
            this.userDataSpan[dataStore].save("isc_userData_" + dataStore);
            this.addToKeyIndex(userKey, dataStore, attr);
        } catch(e) {
            if (isc.Offline.isStorageException(e)) {
                if (savedValue) {
                    this.userDataSpan[dataStore].setAttribute(attr, savedValue);
                } else {
                    this.userDataSpan[dataStore].removeAttribute(attr);
                    this.removeFromKeyIndex(userKey);
                }
            }
            throw e;
        }
    },

    removeValue : function (userKey) {
        var key = this.getDataStoreKey(userKey),
            undef;
        if (key === undef) {
            Offline.logDebug("userData: in removeValue, no value for key '" + userKey + "' was found");
            return;
        }
        var dataStore = ("" + key).substring(0, 1),
            attr = "v" + (("" + key).substring(1) * 1);
        this.userDataSpan[dataStore].removeAttribute(attr);
        this.userDataSpan[dataStore].save("isc_userData_" + dataStore);
        this.removeFromKeyIndex(userKey);
        this.unusedAttributeNumbers[dataStore].push(attr.substring(1) * 1);
    },

    getDataStoreForNewItem : function () {
        // TODO: For now, we'll allocate items to stores on a simple round-robin basis; always
        // allocating to the least-full store is a better approach, but requires us to track
        // additional metrics
        var undef;
        if (this.nextDataStoreToUse === undef) {
            // Select the start point randomly, to avoid overusing the the first few pools in
            // applications that are restarted often, relative to the number of items they store
            this.nextDataStoreToUse = Math.floor(Math.random() * this.poolSize);
        }
        var rtnValue = this.nextDataStoreToUse++;
        if (this.nextDataStoreToUse >= this.poolSize) this.nextDataStoreToUse = 0;
        return rtnValue;
    },

    buildNextAttributeInfo : function () {
        this.nextAttributeNumber = [];
        this.unusedAttributeNumbers = [];
        for (var i = 0; i < this.poolSize; i++) {
            this.unusedAttributeNumbers[i] = [];
            var attrs = this.userDataSpan[i].xmlDocument.firstChild.attributes;
            var work = [];
            for (var j = 0; j < attrs.length; j++) {
                var num = attrs[j].name.substring(1) * 1;
                if (!isNaN(num)) work.add(attrs[j].name.substring(1) * 1);
            }
            if (work.sort) work.sort();
            else this.sort(work);
            var counter = 0;
            for (j = 0; j < work.length; j++) {
                if (work[j] == counter) {
                    counter++;
                    continue;
                }
                while(work[j] != counter && counter <= 9999) {
                    this.unusedAttributeNumbers[i].push(counter++);
                }
                counter++;
            }
            this.nextAttributeNumber[i] = counter;
        }
    },

    //>GWT_Standalone
    sort : function (array) {
        for (var i = 0; i < array.length; i++) {
            var swapped = false;
            for (var j = 1; j < array.length - i; j++) {
                if (array[j] < array[j-1]) {
                    var temp = array[j];
                    array[j] = array[j-1];
                    array[j-1] = temp;
                    swapped = true;
                }
            }
            if (!swapped) break;
        }
    },
    //<GWT_Standalone

    getNextAttributeName : function (dataStore) {

        if (this.unusedAttributeNumbers[dataStore] &&
                this.unusedAttributeNumbers[dataStore].length > 0)
        {
            return "v" + this.unusedAttributeNumbers[dataStore].shift();
        }

        if (this.nextAttributeNumber[dataStore] == null) {
            this.nextAttributeNumber[dataStore] = 1;
        }

        return "v" + this.nextAttributeNumber[dataStore]++;
    },

    addToKeyIndex : function (userKey, dataStore, attr) {
        var keyIndexValue = this.getKeyIndexValue(dataStore, attr);
        this.keyIndex[userKey] = keyIndexValue;
        this.reverseKeyIndex[keyIndexValue] = userKey;
        this.saveKeyIndex();
    },


    removeFromKeyIndex : function (userKey) {
        var keyIndexValue = this.keyIndex[userKey];
        delete this.keyIndex[userKey];
        delete this.reverseKeyIndex[keyIndexValue];
        this.saveKeyIndex();
    },

    saveKeyIndex : function () {
        this.keyIndexStore.setAttribute(this.keyIndexKey, Offline.serialize(this.keyIndex));
        this.keyIndexStore.setAttribute(this.reverseKeyIndexKey, Offline.serialize(this.reverseKeyIndex));
        this.keyIndexStore.save("isc_userData_keyIndex");
    },

    buildReverseKeyIndex : function() {
        this.reverseKeyIndex = {};
        for (var key in this.keyIndex) {
            this.reverseKeyIndex[keyIndex[key]] = key;
        }
    },

    getKeyIndexFromStore : function () {
        //!DONTOBFUSCATE
        var kiText = this.keyIndexStore.getAttribute(this.keyIndexKey);
        if (kiText) {
            eval("var ki = " + kiText);
        } else {
            var ki = null;
        }
        return ki;
    },

    getReverseKeyIndexFromStore : function () {
        //!DONTOBFUSCATE
        var kiText = this.keyIndexStore.getAttribute(this.reverseKeyIndexKey);
        if (kiText) {
            eval("var ki = " + kiText);
        } else {
            var ki = null;
        }
        return ki;
    }

};

if (window.isc) {
    isc.defineClass("Offline").addClassProperties(Offline);
    isc.defineClass("UserDataPersistence").addClassProperties(UserDataPersistence);
} else {

//>GWT_Standalone
isc.addProperties = function (objOne, objTwo) {
    for (var propName in objTwo) objOne[propName] = objTwo[propName];
}

isc.addProperties(isc.Offline, {
    // utilities
    // ---------------------------------------------------------------------------------------
    serialize : function (object) {
        return isc.OfflineJSONEncoder.encode(object);
    },
    logDebug : function (message) {
        if (console) console.log(message);
    },
    logError : function (message) {
        if (console) {
            console.log(message);
        } else {
            alert(message);
        }
    },
    echoLeaf : function (obj) {
        var output = "", undefined;
        if (obj === undefined) return "undef";
        try {
            if (typeof obj == "Array") {
                output += "Array[" + obj.length + "]";
            } else if (typeof obj == "Date") {
                output += "Date(" + obj.toShortDate() + ")";
            } else if (typeof obj == "Function") {
                output += isc.Func.getName(obj, true) + "()";
            } else {
                switch (typeof obj) {
                case "string" :
                    // for shorter strings show the whole thing.  Also, in "longMode" don't
                    // shorten.
                    if (obj.length <= 40) {
                        output += '"' + obj + '"';
                        break;
                    }

                    // for long strings, show an elipsis and the strings full length
                    output += '"' + obj.substring(0, 40) + '..."[' + obj.length + ']';

                    // convert CR/LF to avoid spanning several lines
                    output = output.replaceAll("\n", "\\n").replaceAll("\r", "\\r");
                    break;
                case "object" :
                    // typeof null is "object"
                    if (obj == null) { output += "null"; break; }

                    // DOM object
                    if (obj.tagName != null) {
                        output += "[" + obj.tagName + "Element]";
                        break;
                    }

                    var toString = "" + obj;
                    if (toString != "" && toString != "[object Object]" &&
                        toString != "[object]")
                    {
                        // someone went through the trouble of making a better toString(), so
                        // use it.  NOTE: check for "" because in IE an XmlNodeList among
                        // others will toString() to ""
                        output += toString;
                        break;
                    }

                    // return generic "Obj"
                    output += "Obj";

                    break;
                default: output += "" + obj; // invoke native toString()
                }
            }
            return output;
        } catch (e) {
            var message = "[Error in echoLeaf: " + e + "]";
            output += message;
            return output;
        }
    },

    echo : function (object) { return this.serialize(object); }

});


isc.OfflineJSONEncoder = {

_serialize_remember : function (objRefs, object, path) {
    objRefs.obj.add(object);
    objRefs.path.add(path);
},
_serialize_cleanNode : function (object) {
    var treeId = object["_isc_tree"];
    if (treeId != null) {
        var theTree = window[treeId];
        if (theTree && theTree.parentProperty && object[theTree.parentProperty]) {
            object = theTree.getCleanNodeData(object);
        }
    }
    return object;
},
_serialize_alreadyReferenced : function (objRefs, object) {
    var rowNum = objRefs.obj.indexOf(object);
    if (rowNum == -1) return null;
    return objRefs.path[rowNum];
},
_serialize_addToPath : function (objPath, newIdentifier) {
    if (isc.isA.Number(newIdentifier)) {
        return objPath + "[" + newIdentifier + "]";
    } else if (! isc.Comm._simpleIdentifierRE.test(newIdentifier)) {
        return objPath + '["' + newIdentifier + '"]';
    } else {
        return objPath + "." + newIdentifier;
    }
},

encode : function (object) {
    this.objRefs = {obj:[],path:[]};
    var retVal = this._serialize(object, this.prettyPrint ? "" : null , null);
    this.objRefs = null;
    return retVal
},

dateFormat: "xmlSchema",



encodeDate : function (date) {
    if (this.dateFormat == "dateConstructor") {
        return "new Date(" + date.getTime() + ")";
    } else { // quotes for xml schema
        return '"' + this.toSchemaDate(date) + '"';
    }
},

toSchemaDate : function (date) {
    var dd = "" + date.getDate(),
        mm = "" + (date.getMonth() + 1),
        yyyy = "" + (date.getyear() + 1900),
        hh = "" + date.getHours(),
        mi = "" + date.getMinutes(),
        ss = "" + date.getSeconds();

    dd = dd.length == 1 ? "0" + dd : dd;
    mm = mm.length == 1 ? "0" + mm : mm;
    hh = hh.length == 1 ? "0" + hh : ff;
    mi = mi.length == 1 ? "0" + mi : mi;
    ss = ss.length == 1 ? "0" + ss : ss;

   return yyyy + "-" + mm + "-" + dd + "T" + hh + ":" + mi + ":" + ss;
},

strictQuoting: true,
circularReferenceMode: "path",
circularReferenceMarker: "$$BACKREF$$",
prettyPrint: false,

_serialize : function (object, prefix, objPath) {
    if (!objPath) {
        if (object && object.getID) objPath = object.getID();
        else objPath = "";
    }

    if (object == null) return null;

    // handle simple types
    // In Safari a cross-frame scripting bug means that the 'asSource' method may not always be
    // available as an instance method.
    // call the static version of the same method if this happens.
    if (this.isAString(object)) return this.asSource(object);
    if (this.isAFunction(object)) return null;
    if (this.isANumber(object) || this.isASpecialNumber(object)) return object;
    if (this.isABoolean(object)) return object;
    if (this.isADate(object)) return this.encodeDate(object);

    // skip instances (this is a simplification of the root code)
    if (this.isAnInstance(object)) return null;

    var prevPath = this._serialize_alreadyReferenced(this.objRefs, object);

    if (prevPath != null && objPath.contains(prevPath)) {

        var nextChar = objPath.substring(prevPath.length, prevPath.length+1);
        if (nextChar == "." || nextChar == "[" || nextChar == "]") {
            var mode = this.circularReferenceMode;
            if (mode == "marker") {
                return "'" + this.circularReferenceMarker + "'";
            } else if (mode == "path") {
                return  "'" + this.circularReferenceMarker + ":" + prevPath + "'";
            } else {
                return null;
            }
        }
    }

    if (this.isAClassObject(object)) return null;

    if (object == window) return null;

    this._serialize_remember(this.objRefs, object, objPath);

    // if there is a serialize method associated with this object, call that
    // (NOTE: Leaving this in, in case we're running in a SmartClinet context)
    if (this.isAFunction(object._serialize)) {
        return object._serialize(prefix, this.objRefs, objPath);
    }

    // handle arrays as a special case
    if (this.isAnArray(object))    {
        return this._serializeArray(object, objPath, this.objRefs, prefix);
    }

    var data;
    // if the object has a getSerializeableFields, use whatever it returns, otherwise just use the object
    if (object.getSerializeableFields) {
        data = object.getSerializeableFields([], []);
    } else {
        data = object;
    }
    // and return anything else as a simple object
    return this._serializeObject(data, objPath, this.objRefs, prefix);
},

_serializeArray : function (object, objPath, objRefs, prefix) {
    // Replaced references to the SmartClient StringBuffer class with native strings throughout
    // this method, rather than port that class's functionality. Also removed pretty print
    // facilities - we don't need them for this
    var output = "[";
    // for each element in the array
    for (var i = 0, len = object.length; i < len; i++) {
        var value = object[i];
        if (prefix != null) output += "\r" + prefix;

        var objPath = this._serialize_addToPath(objPath, i);
        var serializedValue =
            this._serialize(value, (prefix != null ? prefix : null), objPath);
        output += serializedValue + ",";
        if (prefix != null) output += " ";
    }
    // get rid of the trailing comma, if any
    var commaChar = output.lastIndexOf(",");
    if (commaChar > -1) output = output.substring(0, commaChar);

    // add the end array marker
    if (prefix != null) output += "\r" + prefix;
    output += "]";

    // and return the output
    return output;
},

_serializeObject : function (object, objPath, objRefs, prefix) {
    // Replaced references to the SmartClient StringBuffer class with native strings throughout
    // this method, rather than port that class's functionality. Also removed pretty print
    // facilities - we don't need them for this
    var output = "{",
        undef;


    object = this._serialize_cleanNode(object);

    // Not trying to support every edge case at this point - skipping code that copes with
    // XMLNodes here, rather than porting isc.echoLeaf
    try {

        for (var key in object) break;
    } catch (e) {
        return null;
    }

    for (var key in object) {
        if (key == null) continue;


        if (key == "xmlHttpRequest") continue;

        // skip internal properties, if the flag is set (simplified this code to remove
        // framework calls)
        if (key.substring(0,1) == "_" || key.substring(0,1) == "$") continue;
        var value = object[key];

        if (this.isAFunction(value)) continue;

        // omit instances entirely if so configured (this code is simplified, we always
        // skip instances)
        if (this.isAnInstance(value)) continue;

        // convert the key to a string
        var keyStr = key.toString();
        // and quote it
        keyStr = '"' + keyStr + '"';

        var objPath = this._serialize_addToPath(objPath, key);
        var serializedValue;
        // don't try to serialize references to GWT Java objects
        if (key != "__ref" && key != "__module") {
            serializedValue =
                this._serialize(value, (prefix != null ? prefix : null), objPath);
        }

        // now output the key : value pair
        if (prefix != null) output += "\r" + prefix;
        output += keyStr + ":" + serializedValue + ",";

        if (prefix != null) output.append(" ");
    }
    // get rid of the trailing comma, if any
    var commaChar = output.lastIndexOf(",");
    if (commaChar > -1) output = output.substring(0, commaChar);

    // add the end object marker
    if (prefix != null) output += "\r" + prefix;
    output += "}";

    // and return the output
    return output;
},

// These methods ripped off from SmartClient's IsA class
isAString : function (object) {
    if (object == null) return false;
    if (typeof object == this._$function) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        return object.constructor.__nativeType == 4;
    }
    if (object.Class != null && object.Class == this._$String) return true;

    return typeof object == "string";
},
isAnArray : function (object) {
    if (object == null) return false;
    if (typeof object == this._$function) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        return object.constructor.__nativeType == 2;
    }
    if (isc.Browser.isSafari) return ""+object.splice == "(Internal function)";
    return ""+object.constructor == ""+Array;
},
isAFunction : function (object) {
    if (object == null) return false;
    if (isc.Browser.isIE && typeof object == this._$function) return true;
    var cons = object.constructor;
    if (cons && cons.__nativeType != null) {
        if (cons.__nativeType != 1) return false;
        if (cons === Function) return true;
    }

    return isc.Browser.isIE ? (isc.emptyString+object.constructor == Function.toString()) :
                              (typeof object == this._$function);
},

isANumber : function (object) {
    if (object == null) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        if (object.constructor.__nativeType != 5) return false;
    } else {
        if (typeof object != "number") return false;
    }
    // it's a number, now check if it's a valid number
    return !isNaN(object) &&
        object != Number.POSITIVE_INFINITY &&
        object != Number.NEGATIVE_INFINITY;
},
isASpecialNumber : function (object) {
    if (object == null) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        if (object.constructor.__nativeType != 5) return false;
    } else {
        if (typeof object != "number") return false;
    }
    return (isNaN(object) || object == Number.POSITIVE_INFINITY ||
            object == Number.NEGATIVE_INFINITY);
},
isABoolean    : function (object) {
    if (object == null) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        return object.constructor.__nativeType == 6;
    }
    return typeof object == "boolean";
},

isADate : function (object) {
    if (object == null) return false;
    if (object.constructor && object.constructor.__nativeType != null) {
        return object.constructor.__nativeType == 3;
    }
    return (""+object.constructor) == (""+Date) &&
            object.getDate && isc.isA.Number(object.getDate())
},
isAnXMLNode : function (object) {
    if (object == null) return false;
    if (isc.Browser.isIE) {
        return object.specified != null && object.parsed != null &&
               object.nodeType != null && object.hasChildNodes != null;
    }
    var doc = object.ownerDocument;
    if (doc == null) return false;
    return doc.contentType == this._$textXML;
},
isAnInstance : function (object) {
    return (object != null && object._scPrototype != null)
},
isAClassObject : function (object) {
    return (object != null && object._isClassObject == true)
},


// This method ripped out of the SmartClient String enhancements
asSource : function (string, singleQuote) {
    if (!this.isAString(string)) string = ""+string;

    var quoteRegex = singleQuote ? String._singleQuoteRegex : String._doubleQuoteRegex,
        outerQuote = singleQuote ? "'" : '"';
    return outerQuote +
               string.replace(/\\/g, "\\\\")
                     // quote whichever quote we use on the outside
                     .replace(quoteRegex, '\\' + outerQuote)
                     .replace(/\t/g, "\\t")
                     .replace(/\r/g, "\\r")
                     .replace(/\n/g, "\\n") + outerQuote;
}

}; // close offline serializer definition

//<GWT_Standalone

} // close !window.isc

})(); // close wrapper function and execute




isc.RPCManager.addClassMethods({


    //>    @classMethod    RPCManager.performOperation()
    // Perform an operation with the default application.<br><br>
    //
    //  @group operations
    //  @see method:performOperation()
    //
    //    @param    operation   (ID)        ID of the operation to perform
    //    @param    data        (any)        data to be passed to the server
    //    @param    callback    (IDACallback)    method to call on operation completion
    //    @param    context     (OperationContext)    context object, which will not go to the server but
    //                                          will be available in the OperationResult passed to
    //                                          the callback
    //    @return    (boolean)
    //      true == operation is underway, false == operation cancelled for some reason, check logs
    // @visibility ida
    //<
    performOperation : function (operation, data, callback, context) {
        isc.rpc.app().performOperation(operation, data, callback, context);
    }
});

isc.DataSource.addClassMethods({

    // ----------------------------------------------------------------------------------------
    // loading remote dataSources

    // dsName -> array of callbacks pending ds load from server
    _getDataSourceCallbacks : {},

    loadSchema : function (name, callback, context) {

        var callbacks = isc.DataSource._getDataSourceCallbacks;
        // if we have a callbacks registry for the named datasource then it is already
        // being fetched from the server and all we need to do is register our callback and wait for
        // the server to reply.  This way we suppress multiple server requests for the same
        // datasource.
        if (callbacks[name]) {
            callbacks[name].add(callback);
            return null;
        }

        // this is the first request for this datasource.
        callbacks[name] = [];
        callbacks[name].add(callback);

        var operationId = name + "_loadSchema";
        context = context || {};
        if (!context.prompt) context.prompt = "loading datasource '" + name + "'";
        context.dsName = name;
        this.logInfo("performing operation: " + operationId);

        isc.rpc.performOperation(operationId, {}, "isc.DS._getDataSourceReply(rpcResponse,data,rpcRequest)",
                                 context);
    },


    _getDataSourceReply : function (response,data, request) {
        //!OBFUSCATEOK
        var noData = data == null || data.length == 0,
            dsName = !noData ? data[0].dsName : request.dsName;
        if (response.status != 0 || noData) {
            isc.DataSource.logError("dynamic datasource fetch failed for datasource '" + dsName + "'. "
                + (isc.isA.String(data) ? data : ""));
            return;
        }

        // register it
        var dsCode = data[0].dsData;

        try {
            isc.eval(dsCode);
        } catch(e) {
            this.logWarn("Error trying to evaluate DataSource creation JS: " + dsCode);
            this.logWarn("Error detail: " + isc.echoAll(e));
        }

        var ds = isc.DS.get(dsName);
        var callbacks = isc.DataSource._getDataSourceCallbacks;
        // call the registered callbacks
        for (var i = 0; i < callbacks[dsName].length; i++) {
            isc.DataSource.logInfo("calling registered callback: " + (callbacks[dsName])[i]);
            this.fireCallback(callbacks[dsName][i], "ds", [ds], ds);
        }

        // clear the callbacks registry for this dataSource.
        delete callbacks[dsName];
    }
});

isc.DataSource.addMethods({

    // performSCServerOperation()
    // Handles SmartClient Server backed dataSource operations.
    performSCServerOperation : function (dsRequest, data) {

        data = this.serializeFields(data, dsRequest);
        dsRequest.oldValues = this.serializeFields(dsRequest.oldValues, dsRequest);

        // make an rpcRequest from the dsRequest
        var rpcRequest = isc.addProperties({}, dsRequest);

        // allow each DataSource to go to a different URL (but note: breaks cross-DataSource
        // multi-op).  NOTE: in the custom, non-ISC server pipeline, this is also allowed, but
        // occurs in getServiceInputs(), where there is more context (including the ability to
        // specify different URLs for different operationBindings)
        var dataURL = this.getDataURL(dsRequest);
        if (dataURL) rpcRequest.actionURL = dataURL;

        // structure expected by ISC backend for DS requests
        // {
        //    appID : "builtinApplication" or the ID of an IDA in an .app.xml file
        //    operation : "[dataSourceId]_[operationType]" or the ID of an operation declared in
        //                an .app.xml file
        //    operationConfig : {
        //       dataSource : [dataSourceId]
        //       operationType : [DSOperationType]
        //       textMatchStyle : "substring" (or absent for exact match)
        //    }
        //    startRow : [startRow]
        //    endRow : [endRow]
        //    criteria : {}
        //    values : {}
        // }

        // split inbound data into 'where' and 'values' as expected by ISC backend
        var operationType = dsRequest.operationType;
        var requestData = rpcRequest.data =
                this.prepareDataSourceData(operationType, data, dsRequest);

        // support "filter" as a synonym to "fetch", passing a flag that switches on
        // substring-style matching
        var textMatchStyle = dsRequest.textMatchStyle;
        if (operationType == "filter") {
            operationType = "fetch";
            // filter rather than fetch implies textMatchStyle:"substring"
            // However if textMatchStyle is explicitly specified as part of the
            // request, respect it

            if (textMatchStyle == null) textMatchStyle = "substring";
        }

        // the operationConfig substructure contains a few flags that affect how the operation
        // is performed.  XXX really, these are no different from eg startRow, and should
        // appear at top level instead
        var opConfig = requestData.operationConfig = {
            dataSource : this.ID,
            repo: this.repo,
            operationType : operationType
        };
        if (textMatchStyle) opConfig.textMatchStyle = textMatchStyle;

        // dsRequest.parameters support (additional data sent separately from the DataSource
        // protocol data).  Old name: context.appInputs.
        isc.addProperties(requestData, dsRequest.appInputs, dsRequest.parameters);

        // startRow, endRow and sortBy all appear at the top level of the request
        if (dsRequest.startRow != null) {
            requestData.startRow = dsRequest.startRow;
        }
        if (dsRequest.endRow != null) {
            requestData.endRow = dsRequest.endRow;
        }
        if (dsRequest.parentNode != null) {
            requestData.parentNode = isc.Tree.getCleanNodeData(dsRequest.parentNode);
        }
        if (dsRequest.sortBy != null) {
            requestData.sortBy = dsRequest.sortBy;
        }
        if (dsRequest.componentId != null) {
            requestData.componentId = dsRequest.componentId;
        }
        if (dsRequest.useStrictJSON != null) {
            requestData.useStrictJSON = dsRequest.useStrictJSON;
        }
        if (dsRequest.generateRelatedUpdates != null) {
            requestData.generateRelatedUpdates = dsRequest.generateRelatedUpdates;
        }
        if (dsRequest.groupBy != null) {
            requestData.groupBy = dsRequest.groupBy;
        }
        if (dsRequest.summaryFunctions != null) {
            requestData.summaryFunctions = dsRequest.summaryFunctions;
        }

        // validationMode and pendingAdd appear at the top level of the request
        if (dsRequest.validationMode != null) {
            requestData.validationMode = dsRequest.validationMode;
        }
        if (dsRequest.pendingAdd != null) {
            requestData.pendingAdd = dsRequest.pendingAdd;
        }

        // rpcRequest.application can be set to cause .app.xml application definitions
        // on the server to be used.
        var appId = rpcRequest.application || isc.RPCManager.getDefaultApplication();
        if (isc.isAn.Object(appId)) appId = appId.ID;

        // rpcRequest.operationId can be set to use a operation definition from an
        // .app.xml file, or for a custom backend, just as a flag to the backend to perform a
        // variation on the usual fetch et al operations.
        // NOTE: old name: context.operation
        var operation = rpcRequest.operation || rpcRequest.operationId || this.ID + "_" + operationType;
        if (isc.isAn.Object(operation)) operation = operation.ID;

        isc.addProperties(requestData, {
            appID : appId,
            operation : operation,
            // send the original values the client received, if available
            oldValues : dsRequest.oldValues
        });

        // If the dsRequest contained an "outputs" property, set this up
        if (dsRequest.outputs) {
            isc.addProperties(requestData, {
                outputs: dsRequest.outputs
            });
        }

        // Ditto additionalOutputs
        if (dsRequest.additionalOutputs) {
            isc.addProperties(requestData, {
                additionalOutputs: dsRequest.additionalOutputs
            });
        }

        // Ditto facets
        if (dsRequest.facets) {
            isc.addProperties(requestData, {
                facets: dsRequest.facets
            });
        }


        // Streaming, progressive loading and export target options
        if (dsRequest.streamResults != null) {
            requestData.streamResults = dsRequest.streamResults;
        }
        if (dsRequest.exportToFilesystem != null) {
            requestData.exportToFilesystem = dsRequest.exportToFilesystem;
        }
        if (dsRequest.exportToClient != null) {
            requestData.exportToClient = dsRequest.exportToClient;
        }
        if (dsRequest.progressiveLoading != null) {
            requestData.progressiveLoading = dsRequest.progressiveLoading;
        }

        if (isc.ResultTree && isc.isA.ResultTree(dsRequest.resultTree)) {
            requestData.resultTreeIdField = dsRequest.resultTree.idField;
            requestData.resultTreeParentIdField = dsRequest.resultTree.parentIdField;
        } else {
            if (dsRequest.resultTreeIdField != null) {
                requestData.resultTreeIdField = dsRequest.resultTreeIdField;
            }
            if (dsRequest.resultTreeParentIdField != null) {
                requestData.resultTreeParentIdField = dsRequest.resultTreeParentIdField;
            }
        }
        if (dsRequest.keepParentsOnFilter != null) {
            requestData.keepParentsOnFilter = dsRequest.keepParentsOnFilter;
        }

        // hang onto the original, client-side DSRequest object since it's much clearer to pass
        // this to various APIs rather than the munged structure we pass to the server
        rpcRequest._dsRequest = dsRequest;

        //>DEBUG
        this.logInfo("performDSOperation(" + operationType + ") "
                     + (isc.isAn.Array(data) ? data.length : 1) + " records"
                     + (isc.rpc.queuing ? ", queuing" : ""));
        //<DEBUG
        rpcRequest.callback = {target:this, methodName:"_handleSCServerReply"};

        // expose dsName, ds operation, startRow/endRow in query string.  Useful for looking at
        // timing output of proxy-based performance testing tools like JMeter where parsing the
        // request to get this data is a pain.
        if (isc.DataSource.addMetaDataToQueryString) {
            if (!rpcRequest.queryParams) rpcRequest.queryParams = {};
            isc.addProperties(rpcRequest.queryParams, {
                ds_name: dsRequest.dataSource,
                ds_op: dsRequest.operationType
            });
            if (dsRequest.startRow != null) {
                isc.addProperties(rpcRequest.queryParams, {
                    ds_startRow: dsRequest.startRow,
                    ds_endRow: dsRequest.endRow
                });
            }
            if (dsRequest.sortBy) rpcRequest.queryParams.ds_sortBy = dsRequest.sortBy;
        }

        if (!this.scServerOperationFulfilledFromOffline(dsRequest)) {
            return isc.RPCManager.sendRequest(rpcRequest);
        }
    },

    scServerOperationFulfilledFromOffline : function (dsRequest) {
        var cachedRequest = dsRequest.unconvertedDSRequest ?
                            dsRequest.unconvertedDSRequest : dsRequest;
        if (this.useOfflineStorage) {
            // XXX: An opportunity to return a locally cached response exists here, by
            // calling isc.DataSource.getCachedResponse(), but we don't yet have a mechanism
            // by which user code can specify that we should bypass the cache.  Revisit.
            if (isc.Offline) {
                // We're offline, so use the cache
                if (isc.Offline.isOffline()) {
                    var dsResponse = isc.Offline.getResponse(cachedRequest);
                    if (dsResponse != null) {
                        if (this.useOfflineResponse &&
                                !this.useOfflineResponse(cachedRequest, dsResponse))
                        {
                            this.logInfo("User-written useOfflineResponse() method returned" +
                                         " false; not using cached response", "offline");
                            dsResponse = null;
                        }
                    }
                    if (dsResponse == null) {
                        dsResponse = {
                            status: isc.RPCResponse.STATUS_OFFLINE,
                            data: []
                        }
                    }
                    dsRequest._dsRequest = dsRequest;
                    this.delayCall("_handleSCServerReply", [dsResponse, dsResponse.data, dsRequest], 0);
                    return true;;
                // We're not offline, but the user asked for cached data anyway
                } else if (cachedRequest.useOfflineCache || cachedRequest.useOfflineCacheOnly) {
                    var dsResponse = isc.Offline.getResponse(cachedRequest);
                    if (dsResponse != null) {
                        if (this.useOfflineResponse &&
                                !this.useOfflineResponse(cachedRequest, dsResponse))
                        {
                            this.logInfo("User-written useOfflineResponse() method returned" +
                                         " false; not using cached response", "offline");
                            dsResponse = null;
                        }
                    }
                    if (dsResponse != null) {
                        dsRequest._dsRequest = dsRequest;
                        this.delayCall("_handleSCServerReply", [dsResponse, dsResponse.data, dsRequest], 0);
                        return true;
                    // We haven't got a matching request in the cache, but the user opted not
                    // to fall back to the server
                    } else if (dsRequest.useOfflineCacheOnly) {
                        dsResponse = {
                            status: isc.RPCResponse.STATUS_OFFLINE,
                            data: []
                        }
                        dsRequest._dsRequest = dsRequest;
                        this.delayCall("_handleSCServerReply", [dsResponse, dsResponse.data, dsRequest], 0);
                        return true;
                    }
                }
            }
        }
        return false;
    },

    // helper fired when SCServer request returns

    _handleSCServerReply : function (rpcResponse, data, rpcRequest) {
        var dsRequest = rpcRequest._dsRequest;
        this._completeResponseProcessing(data, rpcResponse, dsRequest, rpcResponse, rpcRequest);
    },

    // the passed data is assumed to be in raw form (eg form values or result set rows) - we
    // use the datasource operation type to derive what was meant - eg that the user wanted to
    // do a datasource select using a row as a where clause.
    prepareDataSourceData : function (operationType, data, context) {
        var standardType = isc.DS._getStandardOperationType(operationType);
        switch (standardType) {
            case "fetch":
            case "remove":
            case "downloadFile":
            case "viewFile":
                data = { criteria : data };
                break;
            case "add":
            case "replace":
            case "validate":
                data = { values : data };
                break;
            case "update":

                var keyRows = data;
                var values = (context && context.values) ? context.values : data;

                var where = this.filterPrimaryKeyFields(keyRows);
                if (!where) return null;

                if (!values) return null;

                data = { criteria : where, values : values };
                break;
            default:
                data = { values : data };
                break;
        }
        return data;
    }

});

isc.InstantDataApp.addMethods({

    getOperationConfig : function (operation) {
        if (isc.isAn.Object(operation)) return operation;

        // return the operation from the operations array if we have it
        if (this.operations) return this.operations[operation];
    },



    //>    @method    instantDataApp.performOperation()
    //  Perform an operation by sending data to the server and retrieving results.<br><br>
    //
    //  This low-level API can be used to send arbitrary data to the server and retrieve
    //  arbitrary results.  Generally if you are trying to view data in a component, you should
    //  define a DataSource for the data and provide it as the "dataSource" property for the
    //  component; this enables many automatic features including load-on-demand.<br><br>
    //
    //  If you need lower-level access (for example: you need to transform data that comes
    //  from the server before it will be ready for the component), you should probably use the
    //  ResultSet or ResultTree class.<br><br>
    //
    //  Generally, you should only use performOperation() directly if the data being sent or
    //  received is completely free-form and always relatively small.
    //
    //  This method is asynchronous, meaning it returns almost immediately and calls a
    //  "callback" method when the server returns results.  The callback receives one
    //  parameter, an "OperationResult" object.<br><br>
    //
    //    @group    operations
    //  @see class:OperationResult
    //  @see class:ResultSet
    //  @see class:ResultTree
    //
    //    @param    operation   (ID)        ID of the operation to perform
    //    @param    data        (any)        data to be passed to the server
    //    @param    callback    (IDACallback)    method to call on operation completion
    //    @param    context     (OperationContext)    context object, which will not go to the server but
    //                                          will be available in the OperationResult passed to
    //                                          the callback
    //    @return    (boolean)
    //      true == operation is underway, false == operation cancelled for some reason, check logs
    //
    // @visibility ida
    //<
    // XXX Still to describe:
    // - reference to Java<->JS translation
    performOperation : function (operation, data, callback, context) {

        if (!context) context = {};
        else if (!isc.isAn.Object(context)) {
            //>DEBUG
            this.logWarn("performOperation(): passed an invalid context: '" + context + "', ignoring");
            //<DEBUG
            return false;
        }

        context.application = this;

        // BackCompat showPrompt:true for IDA operations by default
        if (context.showPrompt !== false) context.showPrompt = true;
        var operationObj = this.getOperationConfig(operation);
        if (!operationObj) {
            // we assume you are trying to call an operation method you've defined on your
            // backend class, so we create an auto-operation.  If this isn't allowed, the
            // server will reject it.
            // NOTE: currently, this facility (creating an auto-operation any time an
            // undeclared operation is invoked) is also used by server push (Messaging.js) and
            // on-the-fly DataSource loading.
            operation = {ID:operation, source:"auto"};
        } else operation = operationObj;

        if (operation != null) context.operation = operation;
        // prefer a callback passed via the context argument to one passed to this method
        if (callback != null && context.callback == null) context.callback = callback;

        //>DEBUG
        this.logInfo("performOperation(" + operation.ID + ")[" + operation.type + "]:'" +
                     (isc.isAn.Array(data) ? data.length : 1) + " records"
                     + (this.queuing ? ", queuing" : ""));
        //<DEBUG


        // assemble data that will go to the server.
        var requestData = {
            appID : this.ID,
            operation : operation.ID,
            values : data
        };
        // support legacy OperationContext.appInputs (data to send as top-level properties on
        // the request)
        isc.addProperties(requestData, context.appInputs);

        // send the context as the RPCRequest object, since all the RPC control flags
        // previously documented as part of OperationContext are now supported on RPCRequest.
        context.operation = operation;
        context.data = requestData;
        return isc.RPCManager.sendRequest(context);
    }
});

isc.ClassFactory.defineInterface("SCServerEditorActionMethods");

isc.SCServerEditorActionMethods.addInterfaceMethods({

    //>    @method dynamicForm.doExport()
    //
    // Performs a query with the criteria specified from this element's values and returns the
    // results in the form of a downloadable file. Supported formats are CSV (comma separated
    // value) and XML.
    // <P>
    // If this is bound to a dataSource, then a dataSource filter operation will
    // be performed on this dataSource using the values. If a summary argument is provided,
    // then the type of operation will be either a filter or select, to match the query used for
    // the summary widget's data set.
    // <P>
    // The following properties can be set on the context object to specify options for the
    // export:
    // <dl>
    //   <dt>context.exportAs (required)
    //     <dd>the format to export the results as; valid formats are "csv" (comma separated values)
    //     and "xml"
    //   <dt>context.exportFilename (optional)
    //     <dd>the name of the file to save the downloaded results as; if omitted, defaults to
    //     "results.csv" or "results.xml" to match the value of the exportAs parameter
    //   <dt>context.downloadToNewWindow (optional)
    //     <dd>if true, the downloaded file will be opened in a new window, otherwise it will be opened
    //     in the same window; defaults to false unless exportAs is set to "xml" and exportFilename
    //     is omitted, then defaults to true
    // </dl>
    //
    // @param summary (string or object)
    //     the target widget that displays this filter's results, typically a listGrid, specified
    //     as either its ID or the widget itself (optional)
    // @param context (object)
    //     additional parameters for the IDA operation (optional)
    // @visibility internal
    //<

    doExport : function (summary, context) {
        // an export is performed as a normal select or filter operation, with a flag that tells the
        // server we want to download the results
        var operationType = "filter";
        if (summary) {
            // support being passed an ID
            if (isc.isA.String(summary)) summary = window[summary];
            // the export should do a filter or select operation according to the resultSet
            // currently in the summary
            var rs = summary.getData();
            if (rs && isc.isA.ResultSet(rs)) operationType = rs.operation.type;
        }
        context = this.buildRequest(context, operationType);
        var criteria = isc.DynamicForm.getFilterCriteria(this),
            dataSource = this.getDataSource();

        this.doExportWithCriteria(criteria, context.operation, context, dataSource);
    },

    doExportWithCriteria : function (criteria, operation, context, dataSource) {

        context.prompt = (context.prompt || this.doExportPrompt);

        // this operation will result in a file download
        context.downloadResult = true;

        // Note: it seems we can't rely on the mime-type to ensure that the exported file is opened
        // in the correct viewer - therefore set file extension for the common supported cases.
        // See comments in performOperation for why we're setting the 'downloadToNewWindow' var.
        if (context.exportFilename == null) {
            if (operation.exportAs == "csv") {
                context.exportFilename = "results.csv";
                // we can open in the same window as it'll give us the 'save' dialog
                context.downloadToNewWindow = false;
            }
            if (operation.exportAs == "xml") {
                context.exportFilename = "results.xml";
                // open in new window so we don't wipe out the IDA
                context.downloadToNewWindow = true;
            }
        }

        // transform the raw form values into an appropriate set of filter values
        var filterCriteria = isc.DataSource.filterCriteriaForFormValues(criteria);

        return dataSource.performDSOperation(operation.type, filterCriteria, null, context);
    }
});

if (isc.DynamicForm) isc.ClassFactory.mixInInterface("DynamicForm", "SCServerEditorActionMethods");
if (isc.ValuesManager) isc.ClassFactory.mixInInterface("ValuesManager", "SCServerEditorActionMethods");


            /*

            // create a window to target with the download result
            var viewerWindow = window.open("dyna/applications/common/empty.html",
                                           "download", // + isc.timeStamp(),
                                           "WIDTH=" + isc.Page.getWidth() +
                                           ",HEIGHT=" + isc.Page.getHeight() +
                                           ",RESIZABLE,MENUBAR,SCROLLBARS");

            // Trying to detect when the save dialog comes up, in Nav and IE:
            // IE:
            // Assigning onload et al event handler functions to the window doesn't work
            // document.writing out onload et al event handler functions works
            // - download dialog: immediate onload and onBeforeUnload.  onBeforeUnload when save
            //   dialog comes up, nothing after dialog
            // - PDF view: immediate onload and onBeforeUnload.  onBeforeUnload and UNload as PDF
            //   comes up
            // To auto-close window:
            // - on second onBeforeOnload: setTimeout(100, "window.close()")
            // - on unLoad, clear the timeout (preserving the window if a plugin appears in it.
            // Nervous about implementing this because there's no clear reason for two
            // onBeforeUnloads.
            //
            // Nav:
            // Assigning onload et al event handler functions works
            // document.writing out functions does not work
            // - download dialog: immediate UNload,
            //                    onload when download dialog is dismissed
            // - PDF view: either:
            //   - UNload as the PDF comes up, UNload again, weird cursor
            //   - UNload as the PDF comes up, onload afterwards
            // Writing out onload/unload functions works

            var dumpEvent = function () {
                var output = "";
                //for (var prop in window) {
                //    output += prop + ":" + window[prop] + "\n";
                //}
                output += "Event: " + event.type + "\n";
                for (var prop in event) {
                    output += prop + ":" + event[prop] + "\n";
                }
                alert(output);
            }

            var onBeforeUnload = function () {
                if (!window.beforeUnloadCount) window.beforeUnloadCount = 0;
                window.beforeUnloadCount++;
                if (window.beforeUnloadCount == 2) setTimeout("window.close();", 100);
            }
            var onUnload = function () {

            }

            var doc = viewerWindow.document;
            doc.write("<SCRIPT>");
            //doc.write("window.onbeforeunload= " + onBeforeUnload + ";");
            doc.write("window.onload = " + dumpEvent + ";");
            doc.write("window.onbeforeunload= " + dumpEvent + ";");
            doc.write("window.onunload = " + dumpEvent + ";");
            doc.write("</SCRIPT>");

            // temporarily turn on sending in new window.  XXX really, Comm should support this on a
            // per-request basis
            var defaultSetting = isc.Comm.alwaysSendInNewWindow;
            isc.Comm.alwaysSendInNewWindow = true;

            var params = isc.addProperties(transactionParams,
                {transaction:isc.Comm.serialize(transaction.requestData), protocolVersion:"1.0"});
            transaction._window = isc.Comm.sendFieldsToServer({
                URL: URL,
                fields: params,
                frame: viewerWindow
            });

            isc.Comm.alwaysSendInNewWindow = defaultSetting;
            */

            /*
            // APPROACH: in both browsers, make a GET request from the main window targetting the
            // main window

            //alert("GET request in both");

            // - This causes plugin viewers to replace the application page
            // - If the open/save dialog comes up
            //   - IE mangles filename
            //   - IE goes into an odd state where a javascript: URL blows the page away.
            URL = this.transactionAsGetRequest();
            window.location.replace(URL);
            */

            /*
            // APPROACH: in both browsers, target the main window via a pop-up window
            // - This causes plugin viewers to replace the application page

            alert("pop-up in both");
            // works in UM
            // Mae and I both get extra empty request effect when viewing PDFs

            // if the window hasn't been assigned an explicit name, rename it something unique
            if (!window.name) window.name = isc.timeStamp();

            // target the main window
            var target = window.name;
            // temporarily turn on sending in new window.  XXX really, Comm should support this on a
            // per-submit basis
            var defaultSetting = isc.Comm.alwaysSendInNewWindow;
            isc.Comm.alwaysSendInNewWindow = true;

            var params = isc.addProperties(transactionParams,
                {transaction:isc.Comm.serialize(transaction.requestData), protocolVersion:"1.0"});
            transaction._window = isc.Comm.sendFieldsToServer({
                URL:URL,
                target: target
            });

            isc.Comm.alwaysSendInNewWindow = defaultSetting;

            // Immediately clear the "Contacting server..." dialog because we won't get a callback
            // when the file starts downloading
            this.closeOperationPromptWindow(transaction);
            */



isc.Comm.addClassMethods({

//>    @classMethod Comm.sendHiddenFrame()
// Send a group of fields to the server in a hidden frame
//
// @param request (object) object literal containing the various properties of the request:<br>
//
// recognized request keys:
//
// url - url of the submission
// fields - object literal of key/value pairs to send as form post values
// callback - the callback to call when the server responds
//
//
// target - [frame-based comm only] the target frame of the submission
// frame - [frame-based comm only] the frame to use for comm (the one the form will be written
//                                 into)
//
// @visibility internal
//<
sendHiddenFrame : function (request) {

    var URL = request.URL,
        fields = request.fields,
        target = request.target,
        frame = request.frame,
        callback = request.callback,
        resultVarName = request.resultVarName,
        httpMethod = request.httpMethod,
        transactionNum = request.transactionNum,
        transaction = request.transaction
    ;

    // Set up a jscallback on the transaction.requestData object

    this.generateJSCallback(transaction);

    if (request.useSimpleHttp) {
        transaction._clearOnTimeout = true;
    } else {
        // if we're using the SC Server, hang the transaction object on the request's parameter
        // object
        isc.Comm.addTransactionToFields(request);
    }

    // create a hidden frame to send the data in
    if (!frame) frame = isc.HiddenFrame.create();
    frame.transactionNum = transactionNum;

    transaction._window = frame;

    if (callback) {
        //>DEBUG
        if (isc.Browser.isMoz && !resultVarName) {
            isc.rpc.logError("resultVarName argument is required when specifying a callback " +
                          "in Moz");
            return;
        }
        //<DEBUG
        var context = { callback:callback, resultVarName:resultVarName };
        frame.addProperties(context);
    }


    // if a target has not been passed in it will be the iframe we've generated to accept the
    // response.
    if (!target) target = frame.getName();

    var output = "",
        formName = "AUTO_FORM_" + isc.Comm._generatedFormNumber++;

    //>DEBUG
    // output the name of the form for debugging
    output += "Submitting to:<B>" + URL + "</B><BR>";
    //<DEBUG

    // output the form tag
    output += "<FORM ID=" + formName
            + " METHOD=" + (httpMethod ? httpMethod : isc.Comm.sendMethod)
            + " ENCTYPE='application/x-www-form-urlencoded'"
            + " ACTION='" + URL.replace("'","&apos;") + "'"
            + (target != window ? " TARGET='" + target + "'" : "")
            + ">";

    // output each field as a textArea (since we don't know how much text the field
    // contains, this is the best way to ensure that they all go completely).
    for (var fieldName in fields) {
        var value = fields[fieldName];
        if (isc.isA.Function(value)) continue;
        //>DEBUG
        output += "<B>" + fieldName + " :</B><BR>\r&nbsp;&nbsp;"
        //<DEBUG
        output += "<"+"TEXTAREA ROWS=5 COLS=20 NAME=" + fieldName + ">" +
            "<"+"/TEXTAREA><BR>\r";
    }

    // tell the server the name of the frame we're submitting from.  This allows the server to
    // write out script that refers to or manipulates the sending frame (this is used in
    // "reauthentication")
    output += "<INPUT name='__iframeTarget__' type=hidden value='" + target + "'>";

    //
    // output the end form tag
    output += "</FORM>";

    // tell the hidden frame to write the form and submit it

    //>DEBUG
    isc.rpc.logInfo("Sending data via frame " + frame.getID() + " to server at URL: " + URL);
    //<DEBUG
    //this.logWarn("form html: " + output + "\n\n");

    frame.sendForm(output, formName, fields);
    if (transaction) transaction.changed();
    return frame;
},

generateJSCallback : function (transaction) {

    var transactionNum = transaction.transactionNum,
        transactionCallback = transaction.callback,
        jsCallback = isc.Comm.alwaysSendInNewWindow ? "iframeNewWindow" : "iframe";
    transaction.requestData.jscallback = jsCallback;
    this._transactionCallbacks[transactionNum] = transactionCallback;
},


// get the name of a frame that can be targetted by a form submission
// action is a string that will be eval'ed after the frame has been drawn

getTargetableFrame : function (prompt) {
    var target;
    // create a hidden frame to use as the target
    // XXX we could potentially pool these
    target = isc.HiddenFrame.newInstance();
    return target;
},

hiddenFrameReply : function (transactionNum, results,wd) {
    var callback = this._transactionCallbacks[transactionNum];
    delete this._transactionCallbacks[transactionNum];
    if (callback) this.fireCallback(callback, "transactionNum,results,wd", [transactionNum, results, wd]);
},

closeOperationPromptWindow : function (transaction) {
    if (!transaction || transaction.transport != "hiddenFrame") return;


    var wd = transaction._window;

    if (wd == null) {
        // this.logWarn("closeOpPromptWindow - no 'window' passed in");
        return; // for client-only transactions
    }

    transaction._window = null;

    if (isc.isA.HiddenFrame(wd)) {
        // if the comm transaction took place in a hidden frame, destroy the hidden frame
        wd.destroy();
        isc.HiddenFrame._killLoadingIndicators();
    } else { // otherwise just close the comm window
        // could be an XMLHttp request
        if (isc.isA.Function(wd.close)) wd.close();
    }
}

});
// FE treats SCServer as a module, so duplicate what the FA would emit to register the module
// as loaded
isc.module_SCServer=1;


//> @class Rule
// A rule is a declaration that on some triggering event, some standard action will occur.
// Most commonly, a form will change the display or behavior of one or more of its fields.
// Examples include displaying a warning message,
// disabling a field, or populating a field with a calculated value. Some Rules can also act
// upon other components in the application, for example the +link{validatorType,readOnly rule}
// can be used to hide components as well as just FormItems.
// <P>
// A rule is a special-case of a +link{Validator}: validators are designed so that they always
// make sense to run where data is being saved and can either accept or reject the saved data.
// Rules can be triggered by actions other than saving (including button presses) and rather than
// accepting or rejecting values via a +link{validator.condition}, they run some action when
// their trigger event fires, such as displaying informational messages, or
// automatically calculating suggested values for fields.
// <P>
// A validator can be identified as a rule by setting the flag "isRule" on the validator definition.
// <P>
// As with other validators, Rules may be applied directly to fields, or may be applied to the
// +link{RulesEngine.rulesData,rulesData} array of a RulesEngine.
//
//
// @inheritsFrom Validator
// @visibility rules
//<



//> @attr validatorDefinition.isRule (boolean : false : IR)
// Flag indicating validators of this type are +link{Rule}s
// @visibility rules
//<

//> @attr rule.type (RuleType : null : IR)
// Type of rule - see +link{type:RuleType}.  Any +link{type:ValidatorType} is valid as a rule type
// as well.
// @visibility rules
//<


//> @type RuleType
// @value "message" displays an informational or warning message without marking a field invalid
// @value "populate" applies a calculated value to a field based on values in other fields
// @value "setRequired" marks a field as being required
// @visibility rules
//<


//> @attr rule.fieldName (String | Array of String : null : IR)
// This attribute may be used to specify a target field for validators and rules applied
// within a +link{RulesEngine}.
// <P>
// Name of the field that this rule effects.  If this rule affects multiple fields (for example, a
// "readOnly" rule that disables editing on a whole set of fields), either a comma-separated list
// of fieldNames or an Array of fieldNames can be specified.
// @visibility rules
//<


//> @attr rule.locator (AutoTestObjectLocator : null : IR)
// This attribute may be use to specify a target object for rules within a +link{RulesEngine}.
// Only applies to validators specified with the +link{validatorDefinition.isRule} attribute
// set to true.
// <P>
// Locator for target object effected by this rule. This will typically be a locator that resolves
// to a +link{FormItem}, but note that depending on +link{validatorDefinition.supportedTargets},
// other target objects, such as +link{Canvas} instances may be used.
// <P>
// This locator may be specified as an absolute locator derived from
// +link{AutoTest.getObjectLocator()}, or if +link{RulesEngine.baseComponent}
// is specified, a relative locator derived from +link{AutoTest.getRelativeLocator()}
// should be used, and the +link{rulesEngine.baseComponent} will be used to resolve this
// to a live object at runtime.
// @visibility rules
//<


//> @type ValidatorTargetType
// Used by +link{validatorDefinition.supportedTargets}.
// If a rule is specified with a +link{validator.locator}, what object type(s) does the
// validatorDefinition support? The target object will be passed to the +link{ruleDefinition.action()}
// method so that method should be able to handle any specified type being passed in.
// @value FormItem
//  Validator supports FormItem targets. In this case the FormItem and its containerWidget will
//  be passed to the validator action method.
// @value Canvas
//  Validator supports a Canvas as a target. In this case the target canvas will be passed to the
//  ation method. No targetContainer will be passed for simple canvases.
// @value Section
//  Validator supports a SectionStackSection as a target. In this case the target Section will be
//  passed to the action method, and the stack will be passed as the targetContainer parameter.
//
// @visibility rules
//<



//> @attr rule.supportedTargets (Array of String : ["FormItem"] : IR)
// If +link{validator.locator} is used to specify a target object, what target types are
// supported? When the validator or rule is executed, the locator will be resolved to the
// object in question. If the object is of a type listed here, the object will be passed to
// the +link{validator.action} for processing, along with its container if appropriate.
// @visibility rules
//<



//> @attr rule.triggerEvent (TriggerEvent : "submit" : IR)
// Event that triggers this rule.  See +link{TriggerEvent}.
// @visibility rules
//<

//> @type TriggerEvent
// @value "editStart" Rule is triggered each time the form is populated with data (inclusive of
//                    being initialized to +link{dynamicForm.editNewRecord,edit a new record},
//                    such that all fields show their +link{FormItem.defaultValue,default value})
// @value "editStartAndChange" Rule is triggered each time the form is populated with data and
//                    whenever a changed event happens on it's field.
// @value "editorEnter" Rule is triggered when focus enters the field
// @value "editorExit" Rule is triggered when focus leaves the field
// @value "changed" Rule is triggered whenever a changed event happens on it's field
// @value "submit" Rule is triggered when values in the form are being submitted.  This includes
//                 both saving in a form that edits a record, or being submitted as search
//                 criteria in a search form
// @value "manual" Rule is never automatically fired and must be programmatically triggered
// @visibility rules
//<


//> @attr rule.dependentFields (String | Array of String : null : IR)
// For rules that are triggered by user actions in other fields, a list of fieldNames that can
// trigger the rule.  If multiple fields trigger the rule, either a comma-separated list of
// fieldNames or an Array of fieldNames can be specified. Notes:
// <ul>
// <li>If +link{rule.applyWhen} is specified, any fields referenced in the applyWhen conditional
//     are automatically included as implicit dependent fields, in addition to any fields specified
//     here</li>
// <li>If unset, +link{ruleDefinition.getDependentFields()} may be implemented to derive
//     dependent fields directly from the rule. For example the +link{validatorTypes,"populate" rule}
//     has this method defined to derive dependent fields from the rule's specified formula.</li>
// <li>If unset and no <code>getDependentFields()</code> implementation exists, the
//     trigger event will be assumed to come from the +link{rule.fieldName} field</li>
// </ul>
//
// @visibility rules
//<

//> @method validatorDefinition.getDependentFields()
// Optional method to derive dynamic +link{rule.dependentFields,dependentFields} from a rule
// at runtime. This method should return the field names to use.
// @param rule (rule) rule in question
// @param event (TriggerEvent) triggerEvent for the rule in question
// @return (Array of String) dependent field names
// @visibility rules
//<

//> @attr rule.applyWhen (AdvancedCriteria : null : IRA)
// Used to create a conditional rule based on +link{AdvancedCriteria,criteria}.  The rule
// will only be triggered if the criteria match the current form values.
// @visibility rules
//<

//> @class RulesEngine
// The rulesEngine class applies +link{Rule,Rules} to fields displayed across dataBoundComponents
// @visibility rules
//<
isc.defineClass("RulesEngine");

isc.RulesEngine.addProperties({
    init : function () {
        this.Super("init", arguments);
        isc.ClassFactory.addGlobalID(this);

        if (this.members == null) this.members = [];
        else {
            for (var i = 0; i < this.members.length; i++) {
                var member = this.members[i];

                if (isc.isA.String(member)) {
                    this.members[i] = member = window[member];
                }
                this._addMember(member, true);
            }
        }

    },

    //> @attr rulesEngine.baseComponent (Canvas : null : IRW)
    // When finding the target of a rule via +link{rule.locator}, the baseComponent is used as
    // the starting point for resolving the locator if the locator is a relative
    // +link{AutoTestObjectLocator}.  Not required if not using locators, or if
    // locators are absolute.
    // @see AutoTest.getObjectLocator()
    // @see AutoTest.getRelativeObjectLocator()
    //
    // @visibility rules
    //<

    //> @attr rulesEngine.members (Array of DataBoundComponents : null : IRW)
    // Array of +link{DataBoundComponent}s associated with this rulesEngine. The +link{rulesData,rules}
    // for this engine can be triggered by events generated from this set of components and
    // will act upon them, using +link{rule.fieldName} to find the appropriate component and
    // field to interact with.
    // <P>
    // Note that developers may attach members to a rulesEngine either by setting
    // +link{dataBoundComponent.rulesEngine}, or by including the component in the members array
    // for a rulesEngine.
    //
    // @visibility rules
    //<

    //> @method rulesEngine.addMember()
    // Add a member to +link{rulesEngine.members}.
    // @param member (DataBoundComponent) new member to add
    // @visibility rules
    //<
    addMember : function (member) {
        if (!this.members.contains(member)) {
            this.members.add(member);
            this._addMember(member);
        }
    },

    _addMember : function (member) {
        if (member.rulesEngine != this) {

            if (member.rulesEngine != null) {
                member.rulesEngine.removeMember(member);
            }
            member.rulesEngine = this;
        }


    },

    // Notification method that one of our members started editing a new set of values -
    // called from DynamicForm.setValues()
    processEditStart : function (component) {
        this._processComponentTriggerEvent("editStart", component);
        this._processComponentTriggerEvent("editStartAndChanged", component);
    },

    // Notification when a field gets focus (editor enter)
    processEditorEnter : function (component, field) {
        this._processFieldTriggerEvent("editorEnter", component, field);
    },

    // Notification method that a field changed
    // Fired in response to 'changed' not 'change' since we need to extract values from the
    // DBC. Therefore no way to (for example) cancel the change.
    processChanged : function (component, field) {
        this._processFieldTriggerEvent("changed", component, field);
        this._processFieldTriggerEvent("editStartAndChanged", component, field);
    },

    // Notification method that a field lost focus
    processEditorExit : function (component, field) {
        this._processFieldTriggerEvent("editorExit", component, field);
    },

    // Notification that one of our members was submitted (fires after validation at the form level)
    processSubmit : function (component) {
        // In this case we return the result of the validation run. This allows the
        // calling form to cancel submit.

        return this._processComponentTriggerEvent("submit", component);
    },

    // Actual code to fire 'processRules' on for rules associated with a trigger-event.
    _processComponentTriggerEvent : function (eventType, component) {
        var rules = this.rulesData;
        if (!rules || rules.length == 0) return;
        var eventTypeRules = [];

        for (var i = 0; i < rules.length; i++) {
            if (rules[i].triggerEvent == eventType) {
                eventTypeRules[eventTypeRules.length] = rules[i];
            }
        }

        if (eventTypeRules.length > 0) return this.processRules(eventTypeRules);
        return null;

    },

    _processFieldTriggerEvent : function (eventType, component, field) {
        var rules = this.rulesData;
        if (!rules || rules.length == 0) return;
        var eventTypeRules = [];
        for (var i = 0; i < rules.length; i++) {
            if (rules[i].triggerEvent == eventType) {
                var rule = rules[i],
                    ruleDefinition = isc.Validator.getValidatorDefinition(rules[i].type),
                    sourceField = null;

                // Don't crash if an invalid rule type is specified.
                if (!ruleDefinition) {
                    this.logWarn("RulesEngine unable to process rule with invalid type: " + rules[i].type);
                    continue;
                }

                if (rule.dependentFields != null) {
                    sourceField = rule.dependentFields;
                } else if (ruleDefinition.getDependentFields != null) {
                    sourceField = ruleDefinition.getDependentFields(rule, eventType);
                }
                if (sourceField == null || isc.isAn.emptyArray(sourceField)) {
                    // Avoid altering existing rule fieldName array
                    sourceField = (isc.isAn.Array(rule.fieldName) ? rule.fieldName.duplicate() : rule.fieldName);
                }

                if (sourceField != null && !isc.isAn.Array(sourceField)) {
                    sourceField = sourceField.split(",");
                }

                // By default combine "applyWhen" source fields - this ensures if the trigger
                // condition changes we re-evaluate
                if (rule.applyWhen != null) {
                    var includeApplyWhenFields = true;
                    if (rule.dependsOnApplyWhenFields != null) {
                        includeApplyWhenFields = rule.dependsOnApplyWhenFields;
                    } else if (ruleDefinition.dependsOnApplyWhenFields != null) {
                        includeApplyWhenFields = rule.dependsOnApplyWhenFields;
                    }
                    if (includeApplyWhenFields) {
                        var criteriaFields = isc.DataSource.getCriteriaFields(rule.applyWhen);
                        if (sourceField == null) {
                            sourceField = criteriaFields;
                        } else {
                            sourceField.addList(criteriaFields);
                        }
                    }
                }

                if (sourceField) {
                    for (var j = 0; j < sourceField.length; j++) {
                        var dotIndex = sourceField[j].indexOf("."),
                            ds = sourceField[j].substring(0, dotIndex),
                            dsField = sourceField[j].substring(dotIndex+1);

                        if (component.getDataSource() == isc.DataSource.get(ds) &&
                            dsField == field.name)
                        {
                            eventTypeRules.add(rules[i]);
                            // drop out of the inner for-loop
                            break;
                        }
                    }
                }
            }
        }
        if (eventTypeRules.length > 0) return this.processRules(eventTypeRules);
        return null;
    },

    // getValues() Assembles a record values type object comprised of values from all
    // member forms. This will be used by rule / validator logic.
    // Note that for databound forms we store the form values under the dataSource name
    // as an attribute on this object.
    getValues : function () {
        var record = {};
        for (var i = 0; i < this.members.length; i++) {
            var member = this.members[i];
            var values = member.getValues(),
                dataSource = member.getDataSource(),
                dsID = dataSource ? dataSource.getID() : null;

            if (dsID != null) {
                record[dsID] = isc.addProperties(record[dsID] || {}, values);
            } else {

                record.addProperties(values);
            }
        }

        return record;
    },

    //> @method rulesEngine.processRules()
    // Process a set of the rules present in +link{rulesEngine.rulesData}.
    // This method is invoked by the system when the appropriate +link{rule.triggerEvent} occurs
    // on +link{members} of this engine. It may also be called manually, passing in the
    // array of rules to process. This is how rules with <code>triggerEvent</code> set to
    // <code>"manual"</code> are processed.
    // @param rules (Array of Rules) Rules to process
    // @visibility rules
    //<

    processRules : function (rules) {
        if (rules == null) return;

        var values = this.getValues(),
            result = null;
        if (!isc.isAn.Array(rules)) {
            rules = [rules]
        }
        for (var i = 0; i < rules.length; i++) {
//            this.logWarn("processing rule:" + this.echo(rules[i]));
            var rule = rules[i],
                fieldNames = rule.fieldName,
                locator = rule.locator,
                shouldApply = true;
            // If rule.applyWhen is specified we can test this against the fulll set of values,
            // before spinning through individual targets, running the 'performAction' et al.
            if (rule.applyWhen) {
                var criteria = rule.applyWhen;
                // use the static "applyFilter" since we're gathering values from forms in
                // multiple dataSources

                var matchingRows = isc.DataSource.applyFilter([values], criteria);
                if (matchingRows.length == 0) {
                    shouldApply = false;
                }
            }

            // rules can apply to a field (specified as someDS.someFieldName) or to
            // a (relative) locator which will return a SC object -- a component, a
            // FormItem or a SectionStackSection

            // If a locator is used, we need to find the relevant object and call the
            // appropriate API on it.
            // Note that many rule types don't apply to anything other than FormItems

            if (locator != null) {
                // support for multiple locators
                if (isc.isA.String(locator)) locator = locator.split(",");

                for (var j = 0; j < locator.length; j++) {
                    var currentLocator = locator[j],
                        isRelativeLocator = isc.AutoTest.isRelativeLocator(currentLocator),
                        targetObject;
                    if (!isRelativeLocator) {
                        targetObject = isc.AutoTest.getObject(locator);
                    } else {
                        if (this.baseComponent == null) {
                            this.logWarn("RulesEngine has no specified baseComponent. Unable to" +
                                " process rule with specified relative locator:" + currentLocator);
                            continue;
                        }

                        targetObject = isc.AutoTest.resolveRelativeObjectLocator(
                                                this.baseComponent, currentLocator);

                    }
                    if (targetObject == null) {
                        this.logWarn("RulesEngine unable to resolve locator specified on rule. " +
                            (this.baseComponent ? "\nBase Component: " + this.baseComponent : "") +
                            "\nLocator in question:\n" + currentLocator);
                        continue;
                    }


                    // Only "rules" have locators (validators do not).
                    // The distinction is that the validator has no "condition" and so no need
                    // to call "processValidator()" - just call performAction.
                    // However - first verify the targetObject type is supported by the
                    // validator.

                    var targetObjectType,
                        // note that JS requires us to explicitly default this to null or it'll
                        // retain value from the previous run through the loop
                        // (despite being "declared" here)
                        targetContainer = null;
                    if (isc.isA.FormItem(targetObject)) {
                        targetObjectType = "FormItem";
                        targetContainer = targetObject.containerWidget;

                    } else if (targetObject.isSectionHeader) {
                        targetObjectType = "Section";
                        // targetObject will probably be the Canvas but in case
                        // we ever get a pointer to the actual config object, resolve it:
                        if (targetObject.getSectionHeader) {
                            targetObject = targetObject.getSectionHeader();
                        }

                        targetContainer = targetObject.parentElement;
                    } else if (isc.isA.Canvas(targetObject)) {
                        targetObjectType = "Canvas";
                        // no 'container' - we'll just hide the canvas.
                    }


                    isc.Validator.performAction(shouldApply ? true : null,
                        targetObject, rule, values, targetContainer, targetObjectType);
                }
            }

            // Support fieldName being a single fieldName, an array of fieldName strings, or
            // a comma-separated string.
            // Normalize to an array first.
            if (isc.isA.String(fieldNames)) {
                fieldNames = fieldNames.split(",");
            // handle locator with no specified fieldName
            } else if (fieldNames == null) {
                fieldNames = [];
            }
            for (var j = 0; j < fieldNames.length; j++) {
                var fieldName = fieldNames[j],
                    dsName = fieldName.substring(0,fieldName.indexOf(".")),
                    ds = dsName ? isc.DataSource.get(dsName) : null,
                    dsFieldName = fieldName.substring(dsName.length+1),

                    value = isc.DataSource.getPathValue(values, fieldName),

                    componentInfo = this.getComponentInfo(fieldName),
                    component = componentInfo ? componentInfo.component: null,
                    field = componentInfo ? componentInfo.item :
                                (ds ? ds.getField(dsFieldName) : null);
                ;



                if (component == null || field == null) {
                    this.logWarn("RulesEngine contains rule definition with specified fieldName:"
                            + fieldName + " - unable to find associated " +
                             (component == null ? "member component" : "field") + " for this rule.");
                    continue;
                }

                var isValid = null;
                if (shouldApply) {
                    isValid =
                        (isc.Validator.processValidator(field, rule, value, null, values) == true);
                }
                isc.Validator.performAction(isValid, field, rule, values, component);




                if (isValid == false) {
                    result = false;
                    var errorMessage = isc.Validator.getErrorMessage(rule);
                    component.addFieldErrors(field.name, errorMessage, true);
                    this.rememberRuleFieldError(rule, component, field, errorMessage);
                } else {
                    var currentError = this.getRuleFieldError(rule, component, field);

                    if (currentError && component.clearFieldError) {
                        component.clearFieldError(field.name, currentError, true);
                        this.clearRememberedRuleFieldError(rule, component, field);
                    }
                    if (result == null) result = true;
                }
            }
        }
        return result;
    },

    rememberRuleFieldError : function (rule, component, field, errorMessage) {
        // Hang onto the applied error string. We'll selectively clear it below if
        // the triggerEvent runs and validation passes

        if (rule._currentErrors ==  null) {
            rule._currentErrors = {};
        }
        var componentID = component.getID();
        if (rule._currentErrors[componentID] == null) {
            rule._currentErrors[componentID] = {};
        }
        rule._currentErrors[componentID][field.name] = errorMessage;
    },
    getRuleFieldError : function (rule, component, field) {
        var componentID = component.getID();
        if (rule._currentErrors && rule._currentErrors[componentID]
            && rule._currentErrors[componentID][field.name] != null)
        {
            return rule._currentErrors[componentID][field.name];

        }
    },
    clearRememberedRuleFieldError : function (rule, component, field) {
        var componentID = component.getID();
        if (rule._currentErrors && rule._currentErrors[componentID]
            && rule._currentErrors[componentID][field.name] != null)
        {
            delete rule._currentErrors[componentID][field.name];
        }
    },

    // When a rulesEngine applies validators to a field (running in response to a triggerEvent),
    // errors will be applied to the component.
    // If the component is then actually submitted or explicitly 'validated', all errors are
    // cleared and rebuilt from the field.validators
    // This is a notification fired by the DBC when this happens. It gives us a chance to
    // re-run validators for the field in question and re-apply errors if the value would
    // still fail validation.

    applyFieldValidators : function (errors, component) {
        var rules = this.rulesData;
        var values = this.getValues();
        var addedErrors = false;
        for (var i = 0; i < rules.length; i++) {
            var rule = rules[i],
                targetField = rule.fieldName;
            // Assertion: If there's no rule.fieldName this isn't a validator to
            // apply to an item so will be uneffected by the form's "validate()" method
            if (targetField == null) continue;

            var componentInfo = this.getComponentInfo(targetField);
            // Assertion: We are passed the component being validated. Ignore any rules
            // where the target field is not held by the component.
            if (componentInfo == null || componentInfo.component != component) continue;

            var field = componentInfo.item,
                value = isc.DataSource.getPathValue(values, targetField);
            if (!field) return;

            // Drop any "remembered" error that we've applied before. It was already cleared
            // by the logic in the DF code.
            this.clearRememberedRuleFieldError(rule, component, field);

            var shouldApply = true;
            if (rule.applyWhen) {
                var criteria = rule.applyWhen;
                var matchingRows = isc.DataSource.applyFilter([values], criteria);
                if (matchingRows.length == 0) {
                    shouldApply = false;
                }
            }
            // Don't do anything if 'shouldApply' is false. The normal form validation code
            // will have already cleared any error so we can just ignore the validator.
            if (!shouldApply) continue;

            var isValid = isc.Validator.processValidator(field, rule, value, null, values) == true;
            if (!isValid) {
                var errorMessage = isc.Validator.getErrorMessage(rule);
                if (errors[field.name] == null) errors[field.name] = errorMessage;
                else {
                    if (!isc.isAn.Array(errors[field.name])) {
                        errors[field.name] = [errors[field.name]];
                    }
                    errors[field.name].add(errorMessage);
                }
                // Remember the applied error message so we can selectively clear it if necessary.
                this.rememberRuleFieldError(rule, component, field, errorMessage);
                addedErrors = true;
            }
        }

        return addedErrors;
    },

    // Method to take a fieldName (including DS name) like "supplyItem.SKU" and find the
    // associated component and item.
    // Returns an object like {component:<dynamicFormInstance>, item:<formitemInstance>}
    getComponentInfo : function (fieldName) {
        var item,
            index = fieldName.indexOf("."),
            dataSource;
        if (index != -1) {
            dataSource = isc.DataSource.get(fieldName.substring(0, index));
            fieldName = fieldName.substring(index+1);
        }


        for (var i = 0; i < this.members.length; i++) {
            if (this.members[i].getDataSource() == dataSource) {
                item = this.members[i].getItem(fieldName);
                if (item != null) {
                    return {component:this.members[i], item:item};
                }
            }
        }

    },

    //> @method rulesEngine.removeMember()
    // Removes a dataBoundComponent from this engine's +link{members} array.
    // @param member (DataBoundComponent) member to remove
    // @visibility rules
    //<
    removeMember : function (member) {
        if (this.members.contains(member)) {
            this._removeMember(member);
        }
    },
    _removeMember : function (member) {
        this.members.remove(member);
        member.rulesEngine = null;
    },

    setRulesData : function (rulesData) {
        this.rulesData = rulesData;
    }
});



// Resize thumbs
// --------------------------------------------------------------------------------------------

isc.Canvas.addClassProperties({
    resizeThumbConstructor:isc.Canvas,
    resizeThumbDefaults:{
        width:8,
        height:8,
        overflow:"hidden",
        styleName:"resizeThumb",
        canDrag:true,
        canDragResize:true,
        // resizeEdge should be the edge of the target, not the thumb
        getEventEdge : function () { return this.edge; },
        autoDraw:false
    },
    minimumDropMargin: 2,
    minimumDropTargetSize: 10
});

isc.Canvas.addClassMethods({
    // NOTE: Canvas thumbs vs one-piece mask?
    // - since we reuse the same set of thumbs, there's no real performance issue
    // - one-piece mask implementations:
    //   - if an image with transparent regions, thumbs would scale
    //   - if a table
    //     - event handling iffy - transparent table areas may or may not intercept
    //     - would have to redraw on resize
    //   - transparent Canvas with absolutely-positioned DIVs as content
    //     - event handling might be iffy
    // - would have bug: when thumbs are showing, should be able to click between them to hit
    //   something behind the currently selected target
    // - when thumbs are not showing, mask still needs to be there, but would need to shrink and not
    //   show thumbs
    _makeResizeThumbs : function () {
        var edgeCursors = isc.Canvas.getInstanceProperty("edgeCursorMap"),
            thumbs = {},
            thumbClass = isc.ClassFactory.getClass(this.resizeThumbConstructor);
        for (var thumbPosition in edgeCursors) {
           // NOTE: can't use standard autoChild creation because we are in static scope -
           // thumbs are globally shared
           thumbs[thumbPosition] = thumbClass.create({
                ID:"isc_resizeThumb_" + thumbPosition,
                edge:thumbPosition
           }, this.resizeThumbDefaults, this.resizeThumbProperties)
        }
        isc.Canvas._resizeThumbs = thumbs;
    },

    showResizeThumbs : function (target) {
        if (!target) return;

        if (!isc.Canvas._resizeThumbs) isc.Canvas._makeResizeThumbs();

        var thumbSize = isc.Canvas.resizeThumbDefaults.width,
            thumbs = isc.Canvas._resizeThumbs;

        // place the thumbs along the outside of the target
        var rect = target.getPageRect(),
            left = rect[0],
            top = rect[1],
            width = rect[2],
            height = rect[3],

            midWidth = Math.floor(left + (width/2) - (thumbSize/2)),
            midHeight = Math.floor(top + (height/2) - (thumbSize/2));

        thumbs.T.moveTo(midWidth, top - thumbSize);
        thumbs.B.moveTo(midWidth, top + height);
        thumbs.L.moveTo(left - thumbSize, midHeight);
        thumbs.R.moveTo(left + width, midHeight);

        thumbs.TL.moveTo(left - thumbSize, top - thumbSize);
        thumbs.TR.moveTo(left + width, top - thumbSize);
        thumbs.BL.moveTo(left - thumbSize, top + height);
        thumbs.BR.moveTo(left + width, top + height);

        for (var thumbName in thumbs) {
            var thumb = thumbs[thumbName];
            // set all the thumbs to drag resize the canvas we're masking
            thumb.dragTarget = target;
            // show all the thumbs
            thumb.bringToFront();
            thumb.show();
        }

        this._thumbTarget = target;
    },

    hideResizeThumbs : function () {
        var thumbs = this._resizeThumbs;
        for (var thumbName in thumbs) {
            thumbs[thumbName].hide();
        }
        this._thumbTarget = null;
    }

});

// Edit Mask
// --------------------------------------------------------------------------------------------

// At the Canvas level the Edit Mask provides moving, resizing, and standard context menu items.
// The editMask should be extended on a per-widget basis to add things like drop behaviors or
// additional context menu items.  Any such extensions should be delineated with
//>EditMode
//<EditMode
// .. markers so it can be eliminated from normal builds.

isc.Canvas.addProperties({
    editMaskDefaults:{

        // Thumb handling
        // ---------------------------------------------------------------------------------------
        draw : function () {
            this.Super("draw", arguments);

            // stay above the master
            this.observe(this.masterElement, "setZIndex", "observer.moveAbove(observed)");
            // show thumbs on the master as soon as we're draw()n
            isc.Canvas.showResizeThumbs(this);

            // match the master's prompt (native tooltip).  Only actually necessary in Moz since IE
            // considers the eventMask transparent with respect to determining the prompt.
            this.observe(this.masterElement, "setPrompt", "observer.setPrompt(observed.prompt)");

            return this;
        },
        parentVisibilityChanged : function () {
            this.Super("parentVisibilityChanged", arguments);
            if (isc.Canvas._thumbTarget == this) isc.Canvas.hideResizeThumbs();
        },

        // show thumbs when clicked on.  NOTE: since there's only one set of thumbs, this implicitly
        // accomplishes the goal of having only one selected widget
        click : function () {
            isc.Canvas.showResizeThumbs(this);
            return isc.EH.STOP_BUBBLING;
        },

        // Event Bubbling
        // ---------------------------------------------------------------------------------------

        // XXX FIXME: this is here to maintain z-order on dragReposition.  EH.handleDragStop()
        // brings the mask to the front when we stop dragging - which is not what we want, so we
        // suppress it here.
        bringToFront : function () { },

        // prevent bubbling to the editor otherwise we'll start a selection while trying to
        // select/move a component
        mouseDown : function () {
            this.Super("mouseDown", arguments);
            return isc.EH.STOP_BUBBLING;
        },

        mouseUp : function () {
            this.Super("mouseUp", arguments);
            return isc.EH.STOP_BUBBLING;
        },

        doubleClick : function () {
            this._maskTarget.bringToFront();
            return this.click();
        },

        // Drag and drop move and resize
        // ---------------------------------------------------------------------------------------
        // D&D: some awkwardness
        // - if we set dragTarget to the masterElement, it will get the setDragTracker(),
        //   dragRepositionMove() etc events, which it may have overridden, whereas we want just a
        //   basic reposition or resize, so we need to be the dragTarget
        // - to be in the right parental context, and to automatically respond to programmatic
        //   manipulation of the parent's size and position, we want to be a peer, but at the end of
        //   drag interactions we also need to move/resize the master, which would normally cause
        //   the master to move us, so we need to switch off automatic peer behaviors while we move
        //   the master

        // allow the mask to be moved around (only the thumbs allow resize)
        canDrag:true,
        canDragReposition:true,

        // don't allow setDragTracker to bubble in case some parent tries to set it innapropriately
        setDragTracker: function () { return isc.EH.STOP_BUBBLING },

        // when we're moved or resized, move/resize the master and update thumb positions
        moved : function () {
            this.Super("moved", arguments);

            var masked = this.masterElement;
            if (masked) {
                // calculate the amount the editMask was moved
                var deltaX = this.getOffsetLeft() - masked.getLeft();
                var deltaY = this.getOffsetTop() - masked.getTop();

                // relocate our master component (avoiding double notifications)
                this._moveWithMaster = false;
                masked.moveTo(this.getOffsetLeft(), this.getOffsetTop());
                this._moveWithMaster = true;
            }

            if (isc.Canvas._thumbTarget == this) isc.Canvas.showResizeThumbs(this);
        },

        resized : function () {
            this.Super("resized", arguments);

            // Recalculate dropMargin based on new visible size
            this.updateDropMargin();

            // don't loop if we resize master, master overflows, and we resize to overflow'd size
            if (this._resizingMaster) return;
            this._resizingMaster = true;

            var master = this.masterElement;
            if (master) {

                // resize the widget we're masking (avoiding double notifications)
                this._resizeWithMaster = false;
                master.resizeTo(this.getWidth(), this.getHeight());
                this._resizeWithMaster = true;

                // the widget we're masking may overflow, so redraw if necessary to get new size so,
                // and match its overflow'd size
                master.redrawIfDirty();
                this.resizeTo(master.getVisibleWidth(), master.getVisibleHeight());
            }

            // update thumb positions
            isc.Canvas.showResizeThumbs(this);

            this._resizingMaster = false;
        },

        // Editing Context Menus
        // ---------------------------------------------------------------------------------------
        // standard context menu items plus the ability to add "editMenuItems" on the master
        showContextMenu : function () {

            if (!this.editContext) return;

            var edited = this.masterElement,
                menuItems;

            if (this.editContext.selectedComponents.length > 0) { // multi-select
                menuItems =
                        (edited.editMultiMenuItems || []).concat(this.multiSelectionMenuItems);
            } else {
                menuItems = (edited.editMenuItems || []).concat(this.standardMenuItems);
            }

            if (!this.contextMenu) this.contextMenu = this.getMenuConstructor().create({});
            this.contextMenu.setData(menuItems);

            // NOTE: show the menu on our masterElement (the widget we're masking) so that "target"
            // in click methods will be the masterElement and not the mask.
            this.contextMenu.showContextMenu(edited);
            return false;
        },
        standardMenuItems:[
            {title:"Remove", click:"target.destroy()"},
            {title:"Bring to Front", click:"target.bringToFront()"},
            {title:"Send to Back", click:"target.sendToBack()"}
        ],
        multiSelectionMenuItems: [{
            title: "Remove Selected Items",
            click : function (target) {
                if (target != null && isc.isAn.EditPane(target.editContext)) {
                    target.editContext.removeSelection(target);
                }
            }
        }]
    },

    // Enabling EditMode
    // ---------------------------------------------------------------------------------------

    // A hook which subclasses can use if they need to know when they have been added to an editContext
    addedToEditContext : function (editContext, editNode, parentNode, index) {

    },

    //> @method Canvas.updateEditNode()
    // A callback invoked for each +link{EditNode.liveObject,liveObject} by
    // +link{EditContext} when the EditContext is being serialized. The liveObject
    // may make any updates needed to the +link{EditNode} (or the +link{EditContext} as
    // a whole) in order to be able to later recreate the objects.
    // @param editContext (EditContext) the EditContext
    // @param editNode (EditNode) the EditNode
    // @see EditContext.serializeAllEditNodes()
    // @see EditContext.serializeEditNodes()
    // @visibility external
    //<
    updateEditNode : function (editContext, editNode) {

    },

    // A hook called from EditContext.addNode(), allowing the liveParent to wrap a newNode in
    // some additional structure. Return the parentNode that the newNode should be added to.
    // By default, just returns the parentNode supplied.

    wrapChildNode : function (editContext, newNode, parentNode, index) {
        // Add an event mask if so configured
        if (newNode.useEditMask == null && this.autoMaskComponents) {
            newNode.useEditMask = true;
        }
        return parentNode;
    },

    useEditMask:true,
    setEditMode : function (editingOn, editContext, editNode) {
        if (editingOn == null) editingOn = true;
        if (this.editingOn == editingOn) return;
        this.editingOn = editingOn;

        if (this.editingOn) {
            this.editContext = editContext;
        } else {
            this.hideEditMask();
        }

        this.editNode = editNode;

        // If we're going into edit mode, re-route various methods
        if (this.editingOn) {
            this.saveToOriginalValues(["click", "doubleClick", "willAcceptDrop",
                                       "clearNoDropIndicator", "setNoDropCursor", "canAcceptDrop",
                                       "canDropComponents", "drop", "dropMove", "dropOver",
                                       "setDataSource"]);
            this.setProperties({
                click: this.editModeClick,
                doubleClick: this.editModeDoubleClick,
                willAcceptDrop: this.editModeWillAcceptDrop,
                clearNoDropIndicator: this.editModeClearNoDropIndicator,
                setNoDropIndicator: this.editModeSetNoDropIndicator,
                canAcceptDrop: true,
                canDropComponents: true,

                drop: this.editModeDrop,
                dropMove: this.editModeDropMove,
                dropOver: this.editModeDropOver,
                baseSetDataSource: this.setDataSource,
                setDataSource: this.editModeSetDataSource
            });
            // Calculate dropMargin based on visible size
            this.updateDropMargin();
        } else {
            this.restoreFromOriginalValues(["click", "doubleClick", "willAcceptDrop",
                                            "clearNoDropIndicator", "setNoDropCursor", "canAcceptDrop",
                                            "canDropComponents", "drop", "dropMove", "dropOver",
                                            "setDataSource"]);
        }

        // In case anything visual has changed, or the widget has different drag-and-drop
        // behavior in edit mode (register/unregisterDroppableItem is called from redraw)
        this.markForRedraw();
    },


    showEditMask : function () {

        var svgID = this.getID() + ":<br>" + this.src;

        // create an edit mask if we've never created one
        if (!this._editMask) {

            // special SVG handling
            // FIXME: move all SVG-specific handling to SVG.js
            var svgProps = { };
            if (isc.SVG && isc.isA.SVG(this) && isc.Browser.isIE) {
                isc.addProperties(svgProps, {
                    backgroundColor : "gray",
                    mouseOut : function () { this._maskTarget.Super("_hideDragMask"); },
                    contents : isc.Canvas.spacerHTML(10000,10000, svgID)
                });
            }

            var props = isc.addProperties({}, this.editMaskDefaults, this.editMaskProperties,
                                          // assume the editContext is the parent if none is
                                          // provided
                                          {editContext:this.editContext || this.parentElement,
                                           keepInParentRect: this.keepInParentRect},
                                          svgProps);
            this._editMask = isc.EH.makeEventMask(this, props);
        }
        this._editMask.show();

        // SVG-specific
        if (isc.SVG && isc.isA.SVG(this)) {
            if (isc.Browser.isIE) this.showNativeMask();
            else {
                this.setBackgroundColor("gray");
                this.setContents(svgID);
            }
        }
    },
    hideEditMask : function () {
        if (this._editMask) this._editMask.hide();
    },

    editModeClick : function () {
        if (this.editNode) {
            isc.EditContext.selectCanvasOrFormItem(this, true);
            return isc.EH.STOP_BUBBLING;
        }
    },

    editModeDoubleClick : function () {
        // No default impl
    },

    // XXX - Need to do something about Menus in the drop hierarchy - they aren't Class-based

    editModeWillAcceptDrop : function (changeObjectSelection) {
        this.logInfo("editModeWillAcceptDrop for " + this.ID, "editModeDragTarget");
        var dragData = this.ns.EH.dragTarget.getDragData(),
            dragType,
            draggingFromPalette = true;

        // If dragData is null, this is probably because we are drag-repositioning a component
        // in a layout - the dragData is the component itself
        if (dragData == null || (isc.isAn.Array(dragData)) && dragData.length == 0) {
            draggingFromPalette = false;
            this.logInfo("dragData is null - using the dragTarget itself", "editModeDragTarget");
            dragData = this.ns.EH.dragTarget;
            if (isc.isA.FormItemProxyCanvas(dragData)) {
                this.logInfo("The dragTarget is a FormItemProxyCanvas for " + dragData.formItem,
                                "editModeDragTarget");
                dragData = dragData.formItem;
            }
            dragType = dragData._constructor || dragData.Class;
        } else {
            if (isc.isAn.Array(dragData)) dragData = dragData[0];
            dragType = dragData.type || dragData.className;
        }
        this.logInfo("Using dragType " + dragType, "editModeDragTarget");

        if (!this.canAdd(dragType)) {
            this.logInfo(this.ID + " does not accept drop of type " + dragType, "editModeDragTarget");
            // Can't drop on this widget, so check its ancestors
            var ancestor = this.parentElement;
            while (ancestor && !ancestor.editorRoot) {
                if (ancestor.editingOn) {
                    var ancestorAcceptsDrop = ancestor.editModeWillAcceptDrop();
                    if (!ancestorAcceptsDrop) {
                        this.logInfo("No ancestor accepts drop", "editModeDragTarget");
                        if (changeObjectSelection != false) {
                            if (draggingFromPalette) isc.EditContext.hideDragHandle();
                            isc.SelectionOutline.hideOutline();
                            this.setNoDropIndicator();
                        }
                        return false;
                    }
                    this.logInfo("An ancestor accepts drop", "editModeDragTarget");
                    return true;
                }
                // Note that the effect of the return statements in the
                // condition above is that we'll stop walking
                // the ancestor tree at the first parent where editingOn is true ...
                // at that point, we'll re-enter editModeWillAcceptDrop
                ancestor = ancestor.parentElement;
            }

            // Given the return statements in the while condition above, we'll only get
            // here if no ancestor had editingOn: true
            this.logInfo(this.ID + " has no parentElement in editMode", "editModeDragTarget");
            if (changeObjectSelection != false) {
                if (draggingFromPalette) isc.EditContext.hideDragHandle();
                isc.SelectionOutline.hideOutline();
                this.setNoDropIndicator();
            }
            return false;
        }

        // This canvas can accept the drop, so select its top-level parent (in case it's a
        // sub-component like a TabSet's PaneContainer)
        this.logInfo(this.ID + " is accepting the " + dragType + " drop", "editModeDragTarget");
        var hiliteCanvas = this.findEditNode(dragType);
        if (hiliteCanvas) {
            if (changeObjectSelection != false) {
                this.logInfo(this.ID + ": selecting editNode object " + hiliteCanvas.ID);
                if (draggingFromPalette) isc.EditContext.hideDragHandle();
                isc.SelectionOutline.select(hiliteCanvas, false);
                hiliteCanvas.clearNoDropIndicator();
            }
            return true;
        } else {
            this.logInfo("findEditNode() returned null for " + this.ID, "editModeDragTarget");
        }


        if (changeObjectSelection != false) {
            this.logInfo("In editModeWillAcceptDrop, '" + this.ID + "' was willing to accept a '" +
                     dragType + "' drop but we could not find an ancestor with an editNode");
        }
    },

    // Override to provide special editNode canvas selection (note that this impl does not
    // care about dragType, but some special implementations - eg, TabSet - return different
    // objects depending on what is being dragged)
    findEditNode : function (dragType) {
        if (!this.editNode) {
            this.logInfo("Skipping '" + this + "' - has no editNode", "editModeDragTarget");
            if (this.parentElement && this.parentElement.findEditNode) {
                return this.parentElement.findEditNode(dragType);
            } else {
                return null;
            }
        }
        return this;
    },

    // Tests whether this Canvas can accept a child of type "type".  If it can't, and "type"
    // names some kind of FormItem, then we'll accept it if this Canvas is willing to accept
    // a child of type "DynamicForm" -- we'll cope with this downstream by auto-wrapping the dropped
    // FormItem inside a DynamicForm that we create for that very purpose.  Similarly, if
    // the type represents some type of DrawItem then we'll accept the child if this Canvas
    // can a DrawPane.
    canAdd : function (type) {
        if (this.getObjectField(type) == null) {
            var clazz = isc.ClassFactory.getClass(type);
            if (clazz) {
                if (clazz.isA("FormItem")) {
                    return (this.getObjectField("DynamicForm") != null);
                } else if (clazz.isA("DrawItem")) {
                    return (this.getObjectField("DrawPane") != null);
                }
            }
            return false;
        } else {
            return true;
        }
    },

    // Canvas.clearNoDropindicator no-ops if the internal _noDropIndicator flag is null.  This
    // isn't good enough in edit mode because a canvas can be dragged over whilst the no-drop
    // cursor is showing, and we want to revert to a droppable cursor regardless of whether
    // _noDropIndicatorSet has been set on this particular canvas.
    editModeClearNoDropIndicator : function (type) {
        if (this._noDropIndicatorSet) delete this._noDropIndicatorSet;
        this._updateCursor();

        // XXX May need to add support for no-drop drag tracker here if we ever implement
        // such a thing in Visual Builder
    },

    // Special editMode version of setNoDropCursor - again, because the base version no-ops in
    // circumstances where we need it to refresh the cursor.
    editModeSetNoDropIndicator : function () {
        this._noDropIndicatorSet = true;
        this._applyCursor(this.noDropCursor);
    },



    defaultDropMargin: 10,
    defaultDynamicFormDropMargin: 5,
    dropMargin: 10,
    updateDropMargin : function () {
        // Fix up the dropMargin to prevent not-very-tall canvas from passing *every* drop
        // through to parent layouts
        var newDropMargin = (isc.isA.DynamicForm(this) ? this.defaultDynamicFormDropMargin : this.defaultDropMargin);
        if (newDropMargin * 2 > this.getVisibleHeight() - isc.Canvas.minimumDropTargetSize) {
            newDropMargin = Math.round((this.getVisibleHeight() - isc.Canvas.minimumDropTargetSize) / 2);
            if (newDropMargin < isc.Canvas.minimumDropMargin) newDropMargin = isc.Canvas.minimumDropMargin;
        }
        this.dropMargin = newDropMargin;
    },

    shouldPassDropThrough : function () {

        var source = isc.EH.dragTarget,
            paletteNode,
            dropType;

        if (!source.isA("Palette")) {
            dropType = source.isA("FormItemProxyCanvas") ? source.formItem.Class
                                                         : source.Class;
        } else {
            paletteNode = source.getDragData();
            if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
            dropType = paletteNode.type || paletteNode.className;
        }

        this.logInfo("Dropping a " + dropType, "formItemDragDrop");

        if (!this.canAdd(dropType)) {
            this.logInfo("This canvas cannot accept a drop of a " + dropType, "formItemDragDrop");
            return true;
        }

        if (this.parentElement && !this.parentElement.editModeWillAcceptDrop(false)) {
            this.logInfo(this.ID + " is not passing drop through - no ancestor is willing to " +
                        "accept the drop", "editModeDragTarget");
            return false;
        }

        var x = isc.EH.getX(),
            y = isc.EH.getY(),
            work = this.getPageRect(),
            rect = {
                left: work[0],
                top: work[1],
                right: work[0] + work[2],
                bottom:work[1] + work[3]
            }

        if (!this.orientation || this.orientation == "vertical") {
            if (x < rect.left + this.dropMargin  || x > rect.right - this.dropMargin) {
                this.logInfo("Close to right or left edge - passing drop through to parent for " +
                            this.ID, "editModeDragTarget");
                return true;
            }
        }
        if (!this.orientation || this.orientation == "horizontal") {
            if (y < rect.top + this.dropMargin  || y > rect.bottom - this.dropMargin) {
                this.logInfo("Close to top or bottom edge - passing drop through to parent for " +
                            this.ID, "editModeDragTarget");
                return true;
            }
        }

        this.logInfo(this.ID + " is not passing drop through", "editModeDragTarget");
        return false;
    },


    editModeDrop : function () {
        if (this.shouldPassDropThrough()) {
            return;
        }

        var source = isc.EH.dragTarget,
            paletteNode,
            dropType;

        if (!source.isA("Palette")) {
            if (source.isA("FormItemProxyCanvas")) {
                source = source.formItem;
            }
            dropType = source._constructor || source.Class;
        } else {
            paletteNode = source.transferDragData();
            if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
            paletteNode.dropped = true;
            dropType = paletteNode.type || paletteNode.className;
        }

        // if the source isn't a Palette, we're drag/dropping an existing component, so remove the
        // existing component and re-create it in its new position
        if (!source.isA("Palette")) {
            if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
            if (source == this) return;  // Can't drop a component onto itself
            var tree = this.editContext.getEditNodeTree(),
                oldParent = tree.getParent(source.editNode);
            this.editContext.removeNode(source.editNode);
            var node;
            if (source.isA("FormItem")) {
                if (source.isA("CanvasItem")) {
                    node = this.editContext.addNode(source.canvas.editNode, this.editNode);
                } else {
                    node = this.editContext.addWithWrapper(source.editNode, this.editNode);
                }
            } else if (source.isA("DrawItem")) {
                node = this.editContext.addWithWrapper(source.editNode, this.editNode, true);
            } else {
                node = this.editContext.addNode(source.editNode, this.editNode);
            }
            if (node && node.liveObject) {
                isc.EditContext.selectCanvasOrFormItem(node.liveObject, true);
            }
        } else {
            // loadData() operates asynchronously, so we'll have to finish the item drop off-thread
            if (paletteNode.loadData && !paletteNode.isLoaded) {
                var thisCanvas = this;
                paletteNode.loadData(paletteNode, function (loadedNode) {
                    loadedNode = loadedNode || paletteNode;
                    loadedNode.isLoaded = true;
                    thisCanvas.completeItemDrop(loadedNode)
                    loadedNode.dropped = paletteNode.dropped;
                });
                return isc.EH.STOP_BUBBLING;
            }

            this.completeItemDrop(paletteNode);
            return isc.EH.STOP_BUBBLING;
        }
    },

    completeItemDrop : function (paletteNode) {

        if (!this.editContext) return;

        var nodeType = paletteNode.type || paletteNode.className;
        var clazz = isc.ClassFactory.getClass(nodeType);
        if (clazz && clazz.isA("FormItem")) {
            this.editContext.addWithWrapper(paletteNode, this.editNode);
        } else if (clazz && clazz.isA("DrawItem")) {
            this.editContext.addWithWrapper(paletteNode, this.editNode, true);
        } else {
            this.editContext.addNode(paletteNode, this.editNode);
        }
    },

    editModeDropMove : function () {
        if (!this.editModeWillAcceptDrop()) return false;
        if (!this.shouldPassDropThrough()) {
            this.Super("dropMove", arguments);
            if (this.parentElement && this.parentElement.hideDropLine) {
                this.parentElement.hideDropLine();
                if (this.parentElement.isA("FormItem")) {
                    this.parentElement.form.hideDragLine();
                } else if (this.parentElement.isA("DrawItem")) {
                    this.parentElement.drawPane.hideDragLine();
                }
            }
            return isc.EH.STOP_BUBBLING;
        }
    },

    editModeDropOver : function () {
        if (!this.editModeWillAcceptDrop()) return false;
        if (!this.shouldPassDropThrough()) {
            this.Super("dropOver", arguments);
            if (this.parentElement && this.parentElement.hideDropLine) {
                this.parentElement.hideDropLine();
                if (this.parentElement.isA("FormItem")) {
                    this.parentElement.form.hideDragLine();
                } else if (this.parentElement.isA("DrawItem")) {
                    this.parentElement.drawPane.hideDragLine();
                }
            }
            return isc.EH.STOP_BUBBLING;
        }
    },


// DataBoundComponent functionality
// ---------------------------------------------------------------------------------------

// In editMode, when setDataSource is called, generate editNodes for each field so that the
// user can modify the generated fields.
// On change of DataSource, remove any auto-gen field that the user has not changed.

editModeSetDataSource : function (dataSource, fields, forceRebind) {
    //this.logWarn("editMode setDataSource called" + isc.Log.getStackTrace());

    // _loadingNodeTree is a flag set by Visual Builder - its presence indicates that we are
    // loading a view from disk.  In this case, we do NOT want to perform the special
    // processing in this function, otherwise we'll end up with duplicate components in the
    // componentTree.  So we'll just fall back to the base impl in that case.
    if (isc._loadingNodeTree) {
        this.baseSetDataSource(dataSource, fields);
        return;
    }

    if (dataSource == null) return;
    if (dataSource == this.dataSource && !forceRebind) return;

    var fields = this.getFields(),
        keepFields = [],
        removeNodes = [];

    // remove all automatically generated fields that have not been edited by the user

    if (fields) {
        var tree = this.editContext.getEditNodeTree(),
            parentNode = tree.findById(this.ID),
            children = tree.getChildren(parentNode)
        ;
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i],
                editNode = null
            ;
            for (var j = 0; j < children.length; j++) {
                var child = children[j];
                if (field.name == child.name) {
                    editNode = child;
                    break;
                }
            }

            if (editNode && editNode.autoGen && !this.fieldEdited(this, editNode)) {
                removeNodes.add(editNode);
            } else if (editNode) {
                keepFields.add(field);
            }
        }
        this.setFields(keepFields);
        for (var i = 0; i < removeNodes.length; i++) {
            this.editContext.removeNode(removeNodes[i], true);
        }
    }



    // If this dataSource has a single complex field, use the schema of that field in lieu
    // of the schema that was dropped.
    var schema,
        fields = dataSource.fields;
    if (fields && isc.getKeys(fields).length == 1 &&
        dataSource.fieldIsComplexType(fields[isc.firstKey(fields)].name))
    {
        schema = dataSource.getSchema(fields[isc.firstKey(fields)].type);
    } else {
        schema = dataSource;
    }

    // add one editNode for every field in the DataSource that the component would normally
    // display or use.


    var allFields = schema.getFields();
    fields = {};

    for (var key in allFields) {
        var field = allFields[key];
        if (!this.shouldUseField(field, dataSource)) continue;
        fields[key] = allFields[key];
        // duplicate the field on the DataSoure - we don't want to have the live component
        // sharing actual field objects with the DataSource
        fields[key] = isc.addProperties({}, allFields[key]);
    }

    // Merge the list of fields to keep (because they were manually added, or changed after
    // generation) with the list of fields on the new DataSource.  Of course, the "list of
    // fields to keep" could well be the empty list (and always will be if this is the first
    // time we're binding this DataBoundComponent and the user has not manually added fields)
    keepFields.addList(isc.getValues(fields));
    this.baseSetDataSource(dataSource, keepFields);

    for (var key in fields) {
        var field = fields[key];

        // What constitutes a "field" varies by DBC type
        var fieldConfig = this.getFieldEditNode(field, schema);
        var editNode = this.editContext.makeEditNode(fieldConfig);
        //this.logWarn("editMode setDataSource adding field: " + field.name);
        this.editContext.addNode(editNode, this.editNode, null, null, true);
    }
    //this.logWarn("editMode setDataSource done adding fields");
},

// whether a field has been edited
// Strategy: An edited field will likely have more properties than just
// the base "name" and "title". Therefore if there are more properties
// consider the field edited. Otherwise, if the title is different from
// the auto-generated title or from the original DataSource field title
// then the field title has been edited.
fieldEdited : function (parentCanvas, editNode) {
    var edited = false;
    if (editNode.defaults) {
        var defaults = editNode.defaults,
            hasNonBaseProperties = false
        ;
        for (var key in defaults) {
            if (key == "name" || key == "title" || key.startsWith("_")) continue;
            hasNonBaseProperties = true;
            break;
        }
        if (!hasNonBaseProperties) {
            var name = defaults["name"],
                title = defaults["title"]
            ;
            if (title) {
                var dsTitle;
                if (parentCanvas && parentCanvas.dataSource) {
                    var ds = parentCanvas.dataSource;
                    if (isc.isA.String(ds)) ds = isc.DS.getDataSource(ds);
                    if (ds) {
                        var dsField = ds.getField(name)
                        if (dsField) dsTitle = dsField.title;
                    }
                }
                if ((!dsTitle && title != isc.DataSource.getAutoTitle(name)) ||
                        (dsTitle && title != dsTitle))
                {
                    edited = true;
                }
            }
        } else {
            edited = true;
        }
    }
    return edited;
},

// get an editNode from a DataSourceField
getFieldEditNode : function (field, dataSource) {
    // works for ListGrid, TreeGrid, DetailViewer, etc.  DynamicForm overrides
    var fieldType = this.Class + "Field";
    var editNode = {
        type: fieldType,
        autoGen: true,
        defaults: {
            name: field.name,
            // XXX this makes the code more verbose since the title could be left blank and be
            // inherited from the DataSource.  However if we don't supply one here, currently
            // the process of creating an editNode and adding to the editTree generates a title
            // anyway, and without using getAutoTitle().
            title: field.title || dataSource.getAutoTitle(field.name)
        }
    }

    return editNode;
}

});



isc.Class.addMethods({
    getSchema : function () {
        // NOTE: this.schemaName allows multiple classes to share a single role within editing,
        // eg the various possible implementations of tabs, section headers, etc
        if (this.schemaName) return isc.DS.get(this.schemaName);

        // If we have an SGWT class name, then try to get that schema
        var sgwtClassName = this.getSGWTClassName();
        if (sgwtClassName) {
            var schema = isc.DS.get(sgwtClassName);
            if (schema) return schema;
        }

        // If not available, then get the SmartClient class schema
        return isc.DS.get(this.Class);
    },
    getSchemaField : function (fieldName) {
        return this.getSchema().getField(fieldName);
    },
    getObjectField : function (type) {
        if (!isc.SGWTFactory.getFactory(type) && type.contains(".")) type = type.split(/\./).pop();

        // cache lookups, but only on Canvases.  FIXME: we should really cache lookups only for
        // framework DataSources
        var objectFields = this._objectFields;
        if (isc.isA.Canvas(this)) {
            var undef;
            if (objectFields && objectFields[type] !== undef) {
                //this.logWarn("cache hit: " + type);
                return objectFields[type];
            }
        }

        var schema = this.getSchema();
        if (!schema) {
            this.logWarn("getObjectField: no schema exists for: " + this);
            return;
        }
        var fieldName = schema.getObjectField(type);

        if (isc.isA.Canvas(this)) {
            if (!objectFields) this._objectFields = objectFields = {};
            objectFields[type] = fieldName;
        }

        return fieldName;
    },
    addChildObject : function (newChildType, child, index, parentProperty) {
        return this._doVerbToChild("add", newChildType, child, index, parentProperty);
    },
    removeChildObject : function (childType, child, parentProperty) {
        return this._doVerbToChild("remove", childType, child, parentProperty);
    },

    _doVerbToChild : function (verb, childType, child, index, parentProperty) {
        var fieldName = parentProperty || this.getObjectField(childType);
        var field = this.getSchemaField(fieldName);

        // for fields that aren't set to multiple, call setProperties to add the object, which
        // will look up and use the setter if there is one
        // (eg field "contextMenu", "setContextMenu")
        if (!field.multiple) {
            var props = {};
            if (verb == "remove") {
                props[fieldName] = null;
            } else {
                props[fieldName] = child;
            }
            this.logInfo(verb + "ChildObject calling setProperties for fieldName '" + fieldName +
                         "'", "editing");
            this.setProperties(props);
            return true;
        }

        var methodName = this.getFieldMethod(childType, fieldName, verb);
        if (methodName != null) {
            this.logInfo("calling " + methodName + "(" + this.echoLeaf(child) +
                         (index != null ? "," + index + ")" : ")"),
                         "editing");
            this[methodName](child, index);
            return true;
        }

        return false;
    },

    getChildObject : function (type, id, parentProperty) {
        var fieldName = parentProperty || this.getObjectField(type),
            field = this.getSchemaField(fieldName);

        if (field == null) {
            if (parentProperty) {
                this.logWarn("getChildObject: no such field '" + parentProperty +
                             "' in schema: " + this.getSchema());
            } else {
                this.logWarn("getChildObject: schema for Class '" + this.Class +
                             "' does not have a field accepting type: " + type);
            }
            return null;
        }

        // if the field is not array-valued, just use getPropertyValue, which will auto-discover
        // getters
        if (!field.multiple) return this.getPropertyValue(fieldName);

        // otherwise, look for a getter method and call it with the id
        var methodName;

        if (isc.isA.ListGrid(this) && fieldName == "fields") {
            methodName = "getSpecifiedField";
        } else {
            methodName = this.getFieldMethod(type, fieldName, "get");
        }

        if (methodName == null) var methodName = this.getFieldMethod(type, fieldName, "get");
        if (methodName && this[methodName]) {
            this.logInfo("getChildObject calling: " + methodName + "('"+id+"')", "editing");
            return this[methodName](id);
        } else {
            // if there's no getter method, search the Array directly for something with
            // matching id
            this.logInfo("getChildObject calling getArrayItem('"+id+"',this." + fieldName + ")",
                         "editing");
            return isc.Class.getArrayItem(id, this[fieldName]);
        }
    },

    // get a method that can perform verb "verb" for an object of type "type" being added to a
    // field named "fieldName", eg, "add" (verb) a "Tab" (type) to field "tabs".
    // Uses naming conventions to auto-discover methods.  Subclasses may need to override for
    // non-discoverable methods, eg, canvas.addChild() is not discoverable from the field name
    // ("children") or type ("Canvas").
    getFieldMethod : function (type, fieldName, verb) {
        // NOTE: number of args checks: whether it's an add, remove or get, we're looking for
        // something takes arguments, and we don't want to be fooled by eg Class.getWindow()

        var funcName = verb+type;
        // look for add[type] method, e.g. addTab
        if (isc.isA.Function(this[funcName]) &&
            isc.Func.getArgs(this[funcName]).length > 0)
        {
            return funcName;
        }

        // look for add[singular form of field name] method, e.g. addMember
        if (fieldName.endsWith("s")) {
            funcName = verb + this._withInitialCaps(fieldName.slice(0,-1));
            if (isc.isA.Function(this[funcName]) &&
                isc.Func.getArgs(this[funcName]).length > 0)
            {
                return funcName;
            }
        }
    },

    // Returns a copy of a string with the first character uppercased.
    _withInitialCaps : function (s) {
        // Uppercase the first letter, then add the rest.
        return s.substring(0,1).toLocaleUpperCase() + s.substring(1);
    },

    // EditMode OriginalValues
    // ---------------------------------------------------------------------------------------
    // When a component enters editMode it may change appearance or change interactive
    // behavior, for example, a Tab becomes closable via setting canClose.  However if the tab
    // is not intended to be closeable in the actual application, when we edit the tab we want
    // to show canClose as false and if the user changes the value, we want to track that they
    // have changed the value separately from it's temporary setting due to editMode.
    //
    // get/setEditableProperties allows the component to provide specialized properties to a
    // component editor, and saveTo/restoreFromOriginalValues are helpers for a component to
    // track it true, savable state from it's temporary editMode settings

    getEditableProperties : function (fieldNames) {
        var properties = {},
            undef;
        if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
        if (!isc.isAn.Array(fieldNames)) fieldNames = [fieldNames];
        for (var i = 0; i < fieldNames.length; i++) {
            // Just in case we're passed fields rather than names
            var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];

            var value = null;
            if (this.editModeOriginalValues[fieldName] === undef) {
                this.logInfo("Field " + fieldName + " - value [" + this[fieldName] + "] is " +
                        "coming from live values", "editModeOriginalValues");
                value = this[fieldName];
                // If this is an observation notification function, pick up the thing being observed,
                // not the notification function!

                if (isc.isA.Function(value) && value._isObservation) {
                    value = this[value._origMethodSlot];
                }

            } else {
                this.logInfo("Field " + fieldName + " - value [" +
                        this.editModeOriginalValues[fieldName] + "] is coming from " +
                        "original values", "editModeOriginalValues");
                value = this.editModeOriginalValues[fieldName];
            }
            properties[fieldName] = value;
        }

        return properties;
    },

    // called to apply properties to an object when it is edited in an EditContext (eg Visual
    // Builder) via EditContext.setNodeProperties().
    setEditableProperties : function (properties) {
        var undef;
        if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
        for (var key in properties) {
            if (this.editModeOriginalValues[key] === undef) {
                this.logInfo("Field " + key + " - value is going to live values",
                        "editModeOriginalValues");
                this.setProperty(key, properties[key]);
            } else {
                this.logInfo("Field " + key + " - value is going to original values",
                        "editModeOriginalValues");
                this.editModeOriginalValues[key] = properties[key];
            }
        }
        this.editablePropertiesUpdated();
    },

    // called when a child object that is not itself an SC class is having properties applied
    // to it in an EditContext.  Enables cases like a ListGrid handling changes to it's
    // ListGridFields
    setChildEditableProperties : function (liveObject, properties, editNode, editContext) {
        isc.addProperties(liveObject, properties);
    },

    saveToOriginalValues : function (fieldNames) {
        var undef;
        if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
        for (var i = 0; i < fieldNames.length; i++) {
            // Just in case we're passed fields rather than names
            var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];
            if (this[fieldName] === undef) {
                // We'll have to store it as explicit null, otherwise the downstream code won't
                // realize we took a copy
                this.editModeOriginalValues[fieldName] = null;
            } else {
                if (this[fieldName] && this[fieldName]._isObservation) {
                    // Pick up the original method, not the notification function set up by
                    // observation.
                    // If we ever restore the method we want to be restoring the underlying functionality
                    // and not restoring a notification function which may no longer be valid.
                    var origMethodName = isc._obsPrefix + fieldName;
                    this.editModeOriginalValues[fieldName] = this[origMethodName];
                } else {
                    this.editModeOriginalValues[fieldName] = this[fieldName];
                }
            }
        }
    },

    restoreFromOriginalValues : function (fieldNames) {
        var undef;
        if (!this.editModeOriginalValues) this.editModeOriginalValues = {};
        var logString = "Retrieving fields from original values:"
        var changes = {};
        for (var i = 0; i < fieldNames.length; i++) {
            // Just in case we're passed fields rather than names
            var fieldName = isc.isAn.Object(fieldNames[i]) ? fieldNames[i].name : fieldNames[i];
            if (this.editModeOriginalValues[fieldName] !== undef) {
                changes[fieldName] = this.editModeOriginalValues[fieldName];

                // Zap the editModeOriginalValues copy so that future queries will return
                // the live value
                delete this.editModeOriginalValues[fieldName];
            } else {
            }
        }
        // Note use setProperties() rather than just hanging the attributes onto the live
        // widget blindly.
        // Required because:
        // - StringMethods need to be converted to live methods
        // - Observation will be left intact (setProperties/addProperties will correctly update
        //   the renamed underlying method rather than the notification method sitting in its slot)
        // - setProperties will fire propertyChanged which we use in some cases (For example
        //   to update "canDrag" when "canDragRecordsOut" is updated on a ListGrid)

        this.setProperties(changes);
    },

    propertyHasBeenEdited : function (fieldName) {
        var undef;
        if (!this.editModeOriginalValues) return false;
        // Just in case we're passed a field rather than a field name
        if (isc.isAn.Object(fieldName)) fieldName = fieldName.name;
        if (this.editModeOriginalValues[fieldName] !== undef) {
            if (isc.isA.Function(this.editModeOriginalValues[fieldName])) return false;
            if (this.editModeOriginalValues[fieldName] != this[fieldName]) return true;
        }
        return false;
    },

    // Override if you have a class that needs to be notified when editor properties have
    // potentially changed
    editablePropertiesUpdated : function () {
        if (this.parentElement) this.parentElement.editablePropertiesUpdated();
    }

});



isc.DataSource.addClassMethods({

    // Given a parent object and child type, use schema to find out what field children
    // of that type are kept under
    // ---------------------------------------------------------------------------------------
    getSchema : function (object) {
        if (isc.isA.Class(object)) return object.getSchema();
        return isc.DS.get(object.schemaName || object._constructor || object.Class);
    },
    getObjectField : function (object, type) {
        if (object == null) return null;
        if (isc.isA.Class(object)) return object.getObjectField(type);

        var schema = isc.DS.getSchema(object);
        if (schema) return schema.getObjectField(type);
    },
    getSchemaField : function (object, fieldName) {
        var schema = isc.DS.getSchema(object);
        if (schema) return schema.getField(fieldName);
    },

    // Add/remove an object to another object, automatically detecting the appropriate field,
    // and calling add/remove functions if they exist on the parent
    // ---------------------------------------------------------------------------------------
    addChildObject : function (parent, newChildType, child, index, parentProperty) {
        return this._doVerbToChild(parent, "add", newChildType, child, index, parentProperty);
    },
    removeChildObject : function (parent, childType, child, parentProperty) {
        return this._doVerbToChild(parent, "remove", childType, child, parentProperty);
    },
    _doVerbToChild : function (parent, verb, childType, child, index, parentProperty) {
        var fieldName = parentProperty || isc.DS.getObjectField(parent, childType);

        if (fieldName == null) {
            this.logWarn("No field for child of type " + childType);
            return false;
        }

        this.logInfo(verb + " object " + this.echoLeaf(child) +
                     " in field: " + fieldName +
                     " of parentObject: " + this.echoLeaf(parent), "editing");
        var field = isc.DS.getSchemaField(parent, fieldName);

        // if it's a Class, call doVerbToChild on it, which will look for a method that
        // modifies the field
        if (isc.isA.Class(parent)) {
            // if that worked, we're done
            if (parent._doVerbToChild(verb, childType, child, index, parentProperty)) return true;
        }

        // either it's not a Class, or no appropriate method was found, we'll just directly
        // manipulate the properties

        if (!field.multiple) {
            // simple field: "add" is assignment, "remove" is deletion
            if (verb == "add") parent[fieldName] = child;
            else if (verb == "remove") {
                // NOTE: null check avoids creating null slots on no-op removals
                if (parent[fieldName] != null) delete parent[fieldName];
            } else {
                this.logWarn("unrecognized verb: " + verb);
                return false;
            }
            return true;
        }

        this.logInfo("using direct Array manipulation for field '" + fieldName + "'", "editing");

        // Array field: add or remove at index
        var fieldArray = parent[fieldName];
        if (verb == "add") {
            if (fieldArray != null && !isc.isAn.Array(fieldArray)) {
                this.logWarn("unexpected field value: " + this.echoLeaf(fieldArray) +
                             " in field '" + fieldName +
                             "' when trying to add child: " + this.echoLeaf(child));
                return false;
            }
            if (fieldArray == null) parent[fieldName] = fieldArray = [];
            if (index != null) fieldArray.addAt(child, index);
            else fieldArray.add(child);
        } else if (verb == "remove") {
            if (!isc.isAn.Array(fieldArray)) return false;
            if (index != null) fieldArray.removeAt(child, index);
            else fieldArray.remove(child);
        } else {
            this.logWarn("unrecognized verb: " + verb);
            return false;
        }

        return true;
    },

    getChildObject : function (parent, type, id, parentProperty) {
        if (isc.isA.Class(parent)) return parent.getChildObject(type, id, parentProperty);

        var fieldName = isc.DS.getObjectField(parent, type),
            field = isc.DS.getSchemaField(parent, fieldName);


        var value = parent[fieldName];
        //this.logWarn("getting type: " + type + " from field: " + fieldName +
        //             ", value is: " + this.echoLeaf(value));
        if (!field.multiple) return value;

        if (!isc.isAn.Array(value)) return null;
        return isc.Class.getArrayItem(id, value);
    },

    // AutoId: field that should have some kind of automatically assigned ID to make the object
    // referenceable in a builder environment
    // ---------------------------------------------------------------------------------------
    getAutoIdField : function (object) {
        var schema = this.getNearestSchema(object);
        return schema ? schema.getAutoIdField() : "ID";
    },

    getAutoId : function (object) {
        var fieldName = this.getAutoIdField(object);
        return fieldName ? object[fieldName] : null;
    }
});

isc.DataSource.addMethods({
    getAutoIdField : function () {
        return this.getInheritedProperty("autoIdField") || "ID";
    },

    // In the Visual Builder, whether a component should be create()d before being added to
    // it's parent.
    // ---------------------------------------------------------------------------------------
    shouldCreateStandalone : function () {
        if (this.createStandalone != null) return this.createStandalone;
        if (!this.superDS()) return true;
        return this.superDS().shouldCreateStandalone();
    }
});


// Overrides for components that support in-place editing of title
var sharedEditModeFunctions = {
    editModeClick : function () {
        if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "click") this.editClick();
        return this.Super("editModeClick", arguments);
    },
    editModeDoubleClick : function () {
        if (isc.VisualBuilder && isc.VisualBuilder.titleEditEvent == "doubleClick") this.editClick();
        return this.Super("editModeDoubleClick", arguments);
    }
}

isc.Button.addProperties(sharedEditModeFunctions);
isc.ImgButton.addMethods(sharedEditModeFunctions);
isc.StretchImgButton.addMethods(sharedEditModeFunctions);
isc.SectionHeader.addMethods(sharedEditModeFunctions);
isc.ImgSectionHeader.addMethods(sharedEditModeFunctions);

isc.ImgSectionHeader.addMethods({

    setEditMode : function(editingOn, editContext, editNode) {

        if (editingOn == null) editingOn = true;
        if (editingOn == this.editingOn) return;

        this.invokeSuper(isc.TabSet, "setEditMode", editingOn, editContext, editNode);

        if (this.editingOn) {
            // "background" doesn't yet exist - is presumably created asynchronously
            var sectionHeader = this;
            isc.Timer.setTimeout(function () {
                sectionHeader.saveToOriginalValues(["background"]);
                sectionHeader.background.setProperties({
                    iconClick: sectionHeader.editModeIconClick
                })
            }, 0);
        } else {
            this.restoreFromOriginalValues(["background"]);
        }

    },
    editModeIconClick : function () {
        var header = this.creator;
        if (header) {
            var stack = header.layout;
            if (stack.sectionIsExpanded(header)) stack.collapseSection(header);
            else stack.expandSection(header);
            var ctx = header.editContext;
            if (ctx) {
                ctx.setNodeProperties(header.editNode,
                                      {"expanded" : stack.sectionIsExpanded(header)});
            }
        }
        return this.Super("editModeClick", arguments);
    },
    editClick : function () {

        var left = this.getPageLeft() + this.getLeftBorderSize() + this.getLeftMargin() + 1
                                                  - this.getScrollLeft(),
            width = this.getVisibleWidth() - this.getLeftBorderSize() - this.getLeftMargin()
                                 - this.getRightBorderSize() - this.getRightMargin() - 1;

        isc.Timer.setTimeout({target: isc.EditContext,
                              methodName: "manageTitleEditor",
                              args: [this, left, width]}, 100);
    }
});


isc.StatefulCanvas.addMethods({
    editClick : function () {

        var left, width;

        if (isc.isA.Button(this)) {  // This includes Labels and SectionHeaders
            left = this.getPageLeft() + this.getLeftBorderSize() + this.getLeftMargin() + 1
                                                  - this.getScrollLeft();
            width = this.getVisibleWidth() - this.getLeftBorderSize() - this.getLeftMargin()
                               - this.getRightBorderSize() - this.getRightMargin() - 1;
        } else if (isc.isA.StretchImgButton(this)) {
            left = this.getPageLeft() + this.capSize;
            width = this.getVisibleWidth() - this.capSize * 2;
        } else {
            isc.logWarn("Ended up in editClick with a StatefulCanvas of type '" +
                        this.getClass() + "'.  This is neither a Button " +
                        "nor a StretchImgButton - editor will work, but will hide the " +
                        "entire component it is editing");
            left = this.getPageLeft();
            width = this.getVisibleWidth();
        }

        isc.Timer.setTimeout({target: isc.EditContext,
                              methodName: "manageTitleEditor",
                              args: [this, left, width]}, 0);

    },

    // This function is only called for ImgTabs that need to be scrolled into view
    repositionTitleEditor : function () {
        var left = this.getPageLeft() + this.capSize,
            width = this.getVisibleWidth() - this.capSize * 2;

        isc.EditContext.positionTitleEditor(this, left, width);
    }


});


// Edit Mode impl for TabSet
// -------------------------------------------------------------------------------------------
if (isc.TabSet) {
isc.TabSet.addClassProperties({
    addTabEditorHint: "Enter tab titles (comma separated)"
});

isc.TabSet.addProperties({

defaultPaneDefaults: {
    _constructor: "VLayout"
},

setEditMode : function(editingOn, editContext, editNode) {

    if (editingOn == null) editingOn = true;
    if (editingOn == this.editingOn) return;

    this.invokeSuper(isc.TabSet, "setEditMode", editingOn, editContext, editNode);
    // If we're going into edit mode, add close icons to every tab
    if (this.editingOn) {
        for (var i = 0; i < this.tabs.length; i++) {
            var tab = this.tabs[i];
            this.saveOriginalValues(tab);
            this.setCanCloseTab(tab, true);
        }
        this.closeClick = function(tab) {
            this.editContext.removeNode(tab.editNode);
            var tabSet = this;
            isc.Timer.setTimeout(function() {tabSet.manageAddIcon()}, 200);
        }
    } else {
        // If we're coming out of edit mode, revert to whatever was on the init data
        for (var i = 0; i < this.tabs.length; i++) {
            var tab = this.tabs[i];
            this.restoreOriginalValues(tab);
            var liveTab = this.getTab(tab);
            this.setCanCloseTab(tab, liveTab.editNode.defaults.canClose);
        }
    }

    // Set edit mode on the TabBar and PaneContainer.  Note that we deliberately pass null as
    // the editNode - this allows the components to pick up the special editMode method
    // overrides, but prevents them from actually being edited
    this.tabBar.setEditMode(editingOn, editContext, null);
    this.paneContainer.setEditMode(editingOn, editContext, null);

    this.manageAddIcon();

},

saveOriginalValues : function (tab) {
    var liveTab = this.getTab(tab);
    if (liveTab) {
        liveTab.saveToOriginalValues(["closeClick", "canClose", "icon", "iconSize",
                                      "iconOrientation", "iconAlign", "disabled"]);
    }
},

restoreOriginalValues : function (tab) {
    var liveTab = this.getTab(tab);
    if (liveTab) {
        liveTab.restoreFromOriginalValues(["closeClick", "canClose", "icon", "iconSize",
                                           "iconOrientation", "iconAlign", "disabled"]);
    }
},

showAddTabEditor : function () {

    var pos = this.tabBarPosition,
        align = this.tabBarAlign,
        top, left,
        height, width,
        bar = this.tabBar;

    if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
        // Horizontal tabBar
        top = this.tabBar.getPageTop();
        height = this.tabBar.getHeight();
        if (align == isc.Canvas.LEFT) {
            left = this.addIcon.getPageLeft();
            width = this.tabBar.getVisibleWidth() - this.addIcon.left;
            if (width < 150) width = 150;
        } else {
            width = this.tabBar.getVisibleWidth();
            width = width - (width - (this.addIcon.left + this.addIcon.width));
            if (width < 150) width = 150;
            left = this.addIcon.getPageLeft() + this.addIcon.width - width;
        }
    } else {
        // Vertical tabBar
        left = this.tabBar.getPageLeft();
        width = 150;
        top = this.addIcon.getPageTop();
        height = 20;
    }

    this.manageAddTabEditor(left, width, top, height);
},

manageAddIcon : function () {

    if (this.editingOn) {
        if (this.addIcon == null) {
            this.addIcon = isc.Img.create({
                autoDraw: false, width: 16, height: 16,
                cursor: "hand",
                tabSet: this,
                src: "[SKIN]/actions/add.png",
                click: function() {this.tabSet.showAddTabEditor();}
            });
            this.tabBar.addChild(this.addIcon);
        }

        var lastTab = this.tabs.length == 0 ? null : this.getTab(this.tabs[this.tabs.length-1]);
        var pos = this.tabBarPosition,
            align = this.tabBarAlign,
            addIconLeft,
            addIconTop;

        if (lastTab == null) {
            // Empty tabBar
            if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
                // Horizontal tabBar
                if (align == isc.Canvas.LEFT) {
                    addIconLeft = this.tabBar.left + 10;
                    addIconTop = this.tabBar.top + (this.tabBar.height/2) - (8);
                } else {
                    addIconLeft = this.tabBar.left + this.tabBar.width - 10 - (16);  // 16 = icon width
                    addIconTop = this.tabBar.top + (this.tabBar.height/2) - (8);
                }
            } else {
                // Vertical tabBar
                if (align == isc.Canvas.TOP) {
                    addIconLeft = this.tabBar.left + (this.tabBar.width/2) - (8);
                    addIconTop = this.tabBar.top + 10;
                } else {
                    addIconLeft = this.tabBar.left + (this.tabBar.width/2) - (8);
                    addIconTop = this.tabBar.top + this.tabBar.height - 10 - (16)
                }
            }
        } else {
            if (pos == isc.Canvas.TOP || pos == isc.Canvas.BOTTOM) {
                // Horizontal tabBar
                if (align == isc.Canvas.LEFT) {
                    addIconLeft = lastTab.left + lastTab.width + 10;
                    addIconTop = lastTab.top + (lastTab.height/2) - (8);
                } else {
                    addIconLeft = lastTab.left - 10 - (16);  // 16 = icon width
                    addIconTop = lastTab.top + (lastTab.height/2) - (8); // 8 = half icon height
                }
            } else {
                // Vertical tabBar
                if (align == isc.Canvas.TOP) {
                    addIconLeft = lastTab.left + (this.width/2) - (8);
                    addIconTop = lastTab.top + (lastTab.height) + 10;
                } else {
                    addIconLeft = lastTab.left + (this.width/2) - (8);
                    addIconTop = lastTab.top + (lastTab.height/2) - (8);
                }
            }
        }

        this.addIcon.setTop(addIconTop);
        this.addIcon.setLeft(addIconLeft);
        this.addIcon.show();
    } else {
        if (this.addIcon && this.addIcon.hide) this.addIcon.hide();
    }
},

manageAddTabEditor : function (left, width, top, height) {

    if (!isc.isA.DynamicForm(isc.TabSet.addTabEditor)) {
        isc.TabSet.addTabEditor = isc.DynamicForm.create({
            autoDraw: false,
            margin: 0, padding: 0, cellPadding: 0,
            fields: [
                {
                    name: "addTabString", type: "text",
                    hint: isc.TabSet.addTabEditorHint,
                    showHintInField: true,
                    showTitle: false,
                    keyPress : function (item, form, keyName) {
                        if (keyName == "Escape") {
                            form.discardUpdate = true;
                            form.hide();
                            return
                        }
                        if (keyName == "Enter") item.blurItem();
                    },
                    blur : function (form, item) {
                        if (!form.discardUpdate) {
                            form.targetComponent.editModeAddTabs(item.getValue());
                        }
                        form.hide();
                    }
                }
            ]
        });
    }

    var editor = isc.TabSet.addTabEditor;
    editor.addProperties({targetComponent: this});
    editor.discardUpdate = false;

    var item = editor.getItem("addTabString");
    item.setHeight(height);
    item.setWidth(width);
    item.setValue(item.hint);

    editor.setTop(top);
    editor.setLeft(left);
    editor.show();
    item.focusInItem();
    item.delayCall("selectValue", [], 100);
},

editModeAddTabs : function (addTabString) {
    if (!addTabString || addTabString == isc.TabSet.addTabEditorHint) return;
    var titles = addTabString.split(",");
    for (var i = 0; i < titles.length; i++) {
        var tab = {
            type: "Tab",
            defaults: {
                title: titles[i]
            }
        };
        var node = this.editContext.addNode(this.editContext.makeEditNode(tab),
                                                 this.editNode);
        this.editModeAddDefaultPane(node);
    }
},

editModeAddDefaultPane : function (tabNode) {
    if (!tabNode) return;
    var defaultPane = isc.addProperties({}, this.defaultPaneDefaults);
    if (!defaultPane.type && !defaultPane.className) {
        defaultPane.type = defaultPane._constructor;
    }
    this.editContext.addNode(this.editContext.makeEditNode(defaultPane), tabNode);
},

// Extra stuff to do when tabSet.addTabs() is called when the tabSet is in an editable context
// (though not necessarily actually in editMode)
addTabsEditModeExtras : function (newTabs) {

    // Put this on a delay, to give the new tab chance to draw before we start querying its
    // drawn size and position
    this.delayCall("manageAddIcon");

    // If the TabSet is in editMode, put the new tab(s) into edit mode too
    if (this.editingOn) {
        for (var i = 0; i < newTabs.length; i++) {
            this.saveOriginalValues(newTabs[i]);
            this.setCanCloseTab(newTabs[i], true);
        }
    }
},

// Extra stuff to do when tabSet.removeTabs() is called when the tabSet is in an editable
// context (though not necessarily actually in editMode)
removeTabsEditModeExtras : function () {

    // Put this on a delay, to give the new tab chance to draw before we start querying its
    // drawn size and position
    this.delayCall("manageAddIcon");
},

//Extra stuff to do when tabSet.reorderTab() is called when the tabSet is in an editable
//context (though not necessarily actually in editMode)
reorderTabsEditModeExtras : function (originalPosition, moveToPosition) {
    if (this.editContext && this.editContext.reorderNode) {
        this.editContext.reorderNode(this.editNode, originalPosition, moveToPosition);
    }
},

// Override editablePropertiesUpdated() to invoke manageAddIcon() when things change
editablePropertiesUpdated : function () {
    this.delayCall("manageAddIcon");
    this.invokeSuper(isc.TabSet, "editablePropertiesUpdated");
},

tabScrolledIntoView : function () {
    if (!this.editingOn) return;
    for (var i = 0; i < this.tabs.length; i++) {
        var liveTab = this.getTab(this.tabs[i]);
        if (liveTab.titleEditor && liveTab.titleEditor.isVisible()) {
            liveTab.repositionTitleEditor();
        }
    }
},

// Override of Canvas.findEditNode.  If the item being dragged is a Tab, falls back to the
// Canvas impl (which will return the TabSet itself).  If the item being dragged is not a
// Tab, returns the currently selected Tab if it has an editNode, otherwise the first Tab
// with an editNode, otherwise returns the result of calling the parent element's
// findEditNode(), because this is a TabSet with no tabs in edit mode
findEditNode : function (dragType) {
    this.logInfo("In TabSet.findEditNode, dragType is " + dragType, "editModeDragTarget");
    if (dragType != "Tab") {
        var tab = this.getTab(this.getSelectedTabNumber());
        if (tab && tab.editNode) return tab;
        for (var i = 0; i < this.tabs.length; i++) {
            tab = this.getTab(i);
            if (tab.editNode) return tab;
        }
        if (this.parentElement) return this.parentElement.findEditNode(dragType);
    }
    return this.Super("findEditNode", arguments);
},

// Override completeItemDrop() to add the default pane to tabs (and drop into
// edit-title)
completeItemDrop : function (paletteNode, itemIndex, rowNum, colNum, side, callback) {
    this.Super("completeItemDrop", arguments);
    if (paletteNode && (paletteNode.type || paletteNode.className) == "Tab") {
        var liveObj = paletteNode.liveObject;
        this.editModeAddDefaultPane(paletteNode);
        this.selectTab(liveObj);
        liveObj.delayCall("editClick");
    }
}

});

isc.TabBar.addMethods({
    findEditNode : function (dragType) {

        if (dragType == "Tab") {
            // Delegate to the TabSet's findEditNode()
            return this.parentElement.findEditNode(dragType);
        } else if (this.parentElement && isc.isA.Layout(this.parentElement.parentElement)) {
            return this.parentElement.parentElement.findEditNode(dragType);
        }

        return this.Super("findEditNode", arguments);
    }
});
}
// Edit Mode impl for Layout
// -------------------------------------------------------------------------------------------

isc.Layout.addMethods({

// Note that setEditMode() at the Canvas level applies editModeDrop et al to the live canvas.
editModeDrop : function () {

    if (this.shouldPassDropThrough()) {
        this.hideDropLine();
        return;
    }

    isc.EditContext.hideAncestorDragDropLines(this);

    var source = isc.EH.dragTarget,
        paletteNode,
        dropType;

    if (!source.isA("Palette")) {
        if (source.isA("FormItemProxyCanvas")) {
            source = source.formItem;
        }
        dropType = source._constructor || source.Class;
    } else {
        paletteNode = source.transferDragData();
        if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];
        paletteNode.dropped = true;
        dropType = paletteNode.type || paletteNode.className;
    }

    // Establish the actual drop node (this may not be the canvas accepting the drop - for a
    // composite component like TabSet, the dropped-on canvas will be the tabBar or
    // paneContainer)
    var dropTargetNode = this.findEditNode(dropType);
    if (dropTargetNode) {
        dropTargetNode = dropTargetNode.editNode;
    }

    // modifyEditNode() is a late-modify hook for components with unusual drop requirements
    // that don't fit in with the normal scheme of things (SectionStack only, as of August 09).
    // This method can be used to modify the editNode that is going to be the parent - or
    // replace it with a whole different one
    if (this.modifyEditNode) {
        dropTargetNode = this.modifyEditNode(paletteNode, dropTargetNode, dropType);
        if (!dropTargetNode) {
            this.hideDropLine();
            return isc.EH.STOP_BUBBLING;
        }
    }


    // if the source isn't a Palette, we're drag/dropping an existing component, so remove the
    // existing component and re-create it in its new position
    if (!source.isA("Palette")) {
        if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
        if (source == this) return;  // Can't drop a component onto itself
        var tree = this.editContext.getEditNodeTree(),
            oldParent = tree.getParent(source.editNode),
            oldIndex = tree.getChildren(oldParent).indexOf(source.editNode),
            newIndex = this.getDropPosition(dropType);
        this.editContext.removeNode(source.editNode);

        // If we've moved the child component to a slot further down in the same parent,
        // indices will now be off by one because we've just removeed it from its old slot
        if (oldParent == this.editNode && newIndex > oldIndex) newIndex--;
        var node;
        if (source.isA("FormItem")) {
            // If the source is a CanvasItem, unwrap it and insert the canvas into this Layout
            // directly; otherwise, we would end up with teetering arrangments of Canvases in
            // inside CanvasItems inside DynamicForms inside CanvasItems inside DynamicForms...
            if (source.isA("CanvasItem")) {
                node = this.editContext.addNode(source.canvas.editNode, dropTargetNode, newIndex);
            } else {
                // Wrap the FormItem in a DynamicForm
                node = this.editContext.addWithWrapper(source.editNode, dropTargetNode);
            }
        } else if (source.isA("DrawItem")) {
            // Wrap the DrawItem in a DrawPane
            node = this.editContext.addWithWrapper(source.editNode, dropTargetNode, true);
        } else {
            node = this.editContext.addNode(source.editNode, dropTargetNode, newIndex);
        }
        if (isc.isA.TabSet(dropTargetNode.liveObject)) {
            dropTargetNode.liveObject.selectTab(source);
        } else if (node && node.liveObject) {
            isc.EditContext.delayCall("selectCanvasOrFormItem", [node.liveObject, true], 200);
        }
    } else {

        var nodeAdded;
        var clazz = isc.ClassFactory.getClass(dropType);
        if (clazz && clazz.isA("FormItem")) {
            // Create a wrapper form to allow the FormItem to be added to this Canvas
            nodeAdded = this.editContext.addWithWrapper(paletteNode, dropTargetNode);
        } else if (clazz && clazz.isA("DrawItem")) {
            // Create a wrapper form to allow the DrawItem to be added to this Canvas
            nodeAdded = this.editContext.addWithWrapper(paletteNode, dropTargetNode, true);
        } else {
            nodeAdded = this.editContext.addNode(paletteNode, dropTargetNode,
                                          this.getDropPosition(dropType));
        }
        // FIXME - this is almost hackery, needs to be factored more cleanly
        if (nodeAdded != null) {
            var liveObj = paletteNode.liveObject;
            if (isc.isA.TabSet(liveObj)) {
                liveObj.delayCall("showAddTabEditor");
            } else if (isc.isA.ImgTab(liveObj) ||
                       isc.isA.Button(liveObj) ||
                       isc.isA.StretchImgButton(liveObj) ||
                       isc.isA.SectionHeader(liveObj) ||
                       isc.isA.ImgSectionHeader(liveObj)) {
                // Give the object a chance to draw before we start the edit, otherwise the
                // editor co-ordinates will be wrong
                liveObj.delayCall("editClick");
            }
        }
    }

    this.hideDropLine();
    return isc.EH.STOP_BUBBLING;

},

editModeDropMove : function () {
    if (!this.editModeWillAcceptDrop()) return false;
    if (!this.shouldPassDropThrough()) {
        this.Super("dropMove", arguments);
        if (this.parentElement && this.parentElement.hideDropLine) {
            this.parentElement.hideDropLine();
            if (this.parentElement.isA("FormItem")) {
                this.parentElement.form.hideDragLine();
            } else if (this.parentElement.isA("DrawItem")) {
                this.parentElement.drawPane.hideDragLine();
            }
        }
        return isc.EH.STOP_BUBBLING;
    } else {
        this.hideDropLine();
    }
},

editModeDropOver : function () {
    if (!this.editModeWillAcceptDrop()) return false;
    if (!this.shouldPassDropThrough()) {
        this.Super("dropOver", arguments);
        if (this.parentElement && this.parentElement.hideDropLine) {
            this.parentElement.hideDropLine();
            if (this.parentElement.isA("FormItem")) {
                this.parentElement.form.hideDragLine();
            } else if (this.parentElement.isA("DrawItem")) {
                this.parentElement.drawPane.hideDragLine();
            }
        }
        return isc.EH.STOP_BUBBLING;
    } else {
        this.hideDropLine();
    }
}

});


// Edit Mode impl for PortalLayout and friends
// -------------------------------------------------------------------------------------------
//
// Note that PortalLayout and friends have some special features with respect to EditMode.
//
// 1. Even in "live" mode (rather than just "edit" mode), you can drag nodes from a Palette to
//    a PortalLayout and it will do the right thing -- it will create the liveObject from the node,
//    and, if necessary, wrap it in a Portlet. Of course, you have to be in "edit" mode to edit
//    the contents of a Portlet.
//
// 2. The normal user interface of PortalLayout allows the user to adjust the number of columns,
//    move columns around, move Portlets around, etc. Even in "live" mode, the code will adjust
//    the editNodes so that they correspond to the user's actions. You can see this in
//    Visual Builder, for instance, by creating a PortalLayout with some Portlets in "edit" mode,
//    and then switching to "live" mode and moving the Portlets around -- the editNodes will follow.
//
// In order to make this work, there are some bits of code in Portal.js that take account of
// edit mode, but the larger pieces that can be broken out separately are here.

isc.Portlet.addProperties({
    canAdd : function (type) {
        // Don't let Portlets be added directly to Portlets, because it is almost never what
        // would be wanted.
        if (type == "Portlet") return false;
        return this.Super("canAdd", arguments);
    },

    updateEditNode : function (editContext, editNode) {
        if (editContext.persistCoordinates) {
            // We only save if the user has specified a width
            var width = this._percent_width || this._userWidth;
            if (width) {
                editContext.setNodeProperties(editNode, {
                    width: width
                }, true);
            } else {
                editContext.removeNodeProperties(editNode, "width");
            }
        }
    }
});

isc.PortalRow.addProperties({
    updateEditNode : function (editContext, editNode) {
        if (editContext.persistCoordinates) {
            // We only save if the user has specified a height
            var height = this._percent_height || this._userHeight;
            if (height) {
                editContext.setNodeProperties(editNode, {
                    height: height
                }, true);
            } else {
                editContext.removeNodeProperties(editNode, "height");
            }
        }
    },

    wrapChildNode : function (editContext, newNode, parentNode, index) {
        var liveObject = newNode.liveObject;

        if (isc.isA.Portlet(liveObject)) {
            // If it's a portlet, then we're fine
            return parentNode;
        } else {
            // If it's something else, we'll wrap it in a Portlet
            var portletNode = editContext.makeEditNode({
                type: "Portlet",
                defaults: {
                    title: newNode.title,
                    destroyOnClose: true
                }
            });

            editContext.addNode(portletNode, parentNode, index);
            return portletNode;
        }
    },

    // Called from getDropComponent to deal with drops from palettes
    handleDroppedEditNode : function (dropComponent, dropPosition) {
        var editContext = this.editContext;
        var editNode = this.editNode;

        if (isc.isA.Palette(dropComponent)) {
            // Drag and drop from palette
            var data = dropComponent.transferDragData(),
                component = (isc.isAn.Array(data) ? data[0] : data);

            if (editContext && editNode) {
                // If we have an editContext and editNode, just use them. The wrapping
                // is handled by wrapChildNode in this case. We return false to cancel the drop,
                // since addNode will have taken care of it.
                editContext.addNode(component, editNode, dropPosition);
                return false;
            } else {
                // If we don't have an editContext and editNode. then we'll wrap the liveObject
                // in a Portlet if necessary.
                if (isc.isA.Portlet(component.liveObject)) {
                    // If it's a Portlet, we're good
                    dropComponent = component.liveObject;
                } else {
                    // If not, we'll wrap it in one
                    dropComponent = isc.Portlet.create({
                        autoDraw: false,
                        title: component.title,
                        items: [component.liveObject],
                        destroyOnClose: true
                    });
                }
            }
        }

        return dropComponent;
    }
});

isc.PortalColumnBody.addProperties({

    // Called from getDropComponent to deal with drops from palettes
    handleDroppedEditNode : function (dropComponent, dropPosition) {
        var editContext = this.creator.editContext;
        var editNode = this.creator.editNode;

        if (isc.isA.Palette(dropComponent)) {
            // Drag and drop from palette
            var data = dropComponent.transferDragData(),
                component = (isc.isAn.Array(data) ? data[0] : data);

            if (editContext && editNode) {
                // If we have an editContext and editNode, just use them. The wrapping
                // is handled by wrapChildNode in this case. We return false to cancel the drop,
                // since addNode will have taken care of it.
                editContext.addNode(component, editNode, dropPosition);
                return false;
            } else {
                // If we don't have an editContext and editNode, then wrap the liveObject
                // in a Portlet if necessary.
                if (isc.isA.Portlet(component.liveObject)) {
                    // If it's a Portlet, we're good
                    dropComponent = component.liveObject;
                } else {
                    // If not, we'll wrap it in one
                    dropComponent = isc.Portlet.create({
                        autoDraw: false,
                        title: component.title,
                        items: [component.liveObject],
                        destroyOnClose: true
                    });
                }
            }
        }

        if (dropComponent) {
            // We need to check whether the dropComponent is already the only portlet
            // in an existing row. If so, we can simplify by just dropping
            // the row -- that is what the user will have meant.
            var currentRow = dropComponent.portalRow;
            if (currentRow && currentRow.parentElement == this && currentRow.getMembers().length == 1) {
                // Check whether we need to adjust the editNodes
                if (editContext && editNode && currentRow.editNode) {
                    var currentIndex = this.getMemberNumber(currentRow);

                    // Check if we're not really changing position
                    if (dropPosition == currentIndex || dropPosition == currentIndex + 1) return;
                    editContext.removeNode(currentRow.editNode);

                    // Adjust dropPosition if we are dropping after the currentIndex
                    if (currentIndex < dropPosition) dropPosition -= 1;
                    editContext.addNode(currentRow.editNode, editNode, dropPosition);

                    return null;
                }
            } else {
                // If we're not moving a whole current row, then we add the new portlet, creating a new row
                if (editContext && editNode && dropComponent.editNode) {
                    editContext.addNode(dropComponent.editNode, editNode, dropPosition);
                    return null;
                }
            }
        }

        // We'll get here if we're not doing something special with the dropComponent's editNode ...
        // in that case, we can return it and getDropComponent can handle it.
        return dropComponent;
    }
});

isc.PortalColumn.addProperties({
    wrapChildNode : function (editContext, newNode, parentNode, index) {
        var liveObject = newNode.liveObject;

        if (isc.isA.PortalRow(liveObject) || newNode.type == "PortalRow") {
            // If it's a PortalRow, then we're fine
            return parentNode;
        } else if (isc.isA.Portlet(liveObject)) {
            // If it's a portlet, then we'll wrap it in a row
            var rowNode = editContext.makeEditNode({
                type: this.rowConstructor,
                defaults: {}
            });
            editContext.addNode(rowNode, parentNode, index);
            return rowNode;
        } else {
            // If it's something else, we'll wrap it in a Portlet
            var portletNode = editContext.makeEditNode({
                type: "Portlet",
                defaults: {
                    title: newNode.title,
                    destroyOnClose: true
                }
            });
            // Note that when we add the Portlet node, we'll eventually
            // get back here to wrap it in a PortalRow, so we don't need
            // to take care of that explicitly (though we could).
            editContext.addNode(portletNode, parentNode, index);
            return portletNode;
        }
    },

    updateEditNode : function (editContext, editNode) {
        if (editContext.persistCoordinates) {
            // We only save if the user has specified a width
            var width = this._percent_width || this._userWidth;
            if (width) {
                editContext.setNodeProperties(editNode, {
                    width: width
                }, true);
            } else {
                editContext.removeNodeProperties(editNode, "width");
            }
        }
    },

    // We don't actually want to add anything via drag & drop ... that will be
    // handled by PortalColumnBody
    canAdd : function (type) {
        return false;
    },

    setEditMode : function (editingOn, editContext, editNode) {
        this.Super("setEditMode", arguments);

        // We need to put the body in editMode, because the drag/drop behaviours belong there.
        this.rowLayout.setEditMode(editingOn, editContext, null);
    }
});

isc.PortalLayout.addProperties({
    // We need to do some special things when we learn of our EditContext and EditNode
    addedToEditContext : function (editContext, editNode) {
        // We may need to add our PortalColumns to the EditContext, since they may have already been created.
        for (var i = 0; i < this.getNumColumns(); i++) {
            var column = this.getPortalColumn(i);

            if (!column.editContext) {
                // Create the editNode, supplying the liveObject
                var node = editContext.makeEditNode({
                    type: this.columnConstructor,
                    liveObject: column,
                    defaults: {
                        ID: column.ID,
                        _constructor: this.columnConstructor
                    }
                });

                // Add it to the EditContext, without adding the liveObject to the parent, since it's
                // already there.
                editContext.addNode(node, editNode, i, null, true);
            }
        }

        // And we should change our defaults to specify numColumns: 0, because otherwise we'll
        // initialize the default 2 columns when restored, which isn't what will be wanted
        editNode.defaults.numColumns = 0;
    }
});


// Edit Mode impl for DynamicForm
// -------------------------------------------------------------------------------------------
if (isc.DynamicForm) {

isc.DynamicForm.addProperties({

setEditMode : function(editingOn, editContext, editNode) {

    if (editingOn == null) editingOn = true;
    if (editingOn == this.editingOn) return;

    this.invokeSuper(isc.DynamicForm, "setEditMode", editingOn, editContext, editNode);

    // Canvas level implementation already switched on ability to drop components.
    // Add ability to drop items / add columns
    if (this.editingOn) {
        this.saveToOriginalValues(["canDropItems", "canAddColumns", "dropOut"]);
        this.setProperties({
            canDropItems: true,
            canAddColumns: true,
            dropOut: this.editModeDropOut
        });
        // Throw away anything the user might have typed in live mode
        this.resetValues();
    } else {
        // If we're coming out of edit mode, revert to whatever we saved
        this.restoreFromOriginalValues(["canDropItems", "canAddColumns", "dropOut"]);
        // Set default values from whatever the user typed into the formItems in edit mode
        this.resetValues();
    }
},

// editModeDropOver et al picked up by Canvas level setEditMode() implementation.
editModeDropOver : function () {
    if (this.canDropItems != true) return false;
    if (!this.editModeWillAcceptDrop()) return false;
    this._lastDragOverItem = null;
    // just to be safe
    this.hideDragLine();
    return isc.EH.STOP_BUBBLING;
},

editModeDropMove : function () {

    if (!this.ns.EH.getDragTarget()) return false;
    if (this.canDropItems != true) return false;
    if (!this.editModeWillAcceptDrop()) return false;

    // DataSource is a special case - we accept drop, but show no drag line
    var item = this.ns.EH.getDragTarget().getDragData();
    if (isc.isAn.Array(item)) item = item[0];
    if (item && (item.type || item.className) == "DataSource") {
        this.hideDragLine();
        return isc.EH.STOP_BUBBLING;
    }

    // If drop will be passed through, don't show the drag line at all
    if (this.shouldPassDropThrough()) {
        this.hideDragLine();
        return;
    }

    // If the form has no items, indicate insertion at the left of the form
    if (this.getItems().length == 0) {
        isc.EditContext.hideAncestorDragDropLines(this);
        this.showDragLineForForm();
        return isc.EH.STOP_BUBBLING;
    }

    var event = this.ns.EH.lastEvent,
        dropItem = this.getNearestItem(event.x, event.y);

    //if (this._lastDragOverItem && this._lastDragOverItem != dropItem) {
        // still over an item but not the same one
    //}

    if (dropItem) {
        isc.EditContext.hideAncestorDragDropLines(this);
        this.showDragLineForItem(dropItem, event.x, event.y);
    } else {
        this.hideDragLine();
    }

    this._lastDragOverItem = dropItem;

    return isc.EH.STOP_BUBBLING;
},

editModeDropOut : function () {
    this.hideDragLine();
    return isc.EH.STOP_BUBBLING;
},

editModeDrop : function () {
    // DataSource is a special case - it's the only non-visual property that users can drag
    // and drop and a position within the form doesn't make sense
    var dropItem = this.ns.EH.getDragTarget().getDragData();
    if (isc.isAn.Array(dropItem)) dropItem = dropItem[0];
    if ((dropItem && (dropItem.type || dropItem.className) == "DataSource") ||
        this.getItems().length == 0)                       // Empty form is also a special case
    {
        if (this.shouldPassDropThrough()) {
            this.hideDragLine();
            return;
        }
        this.itemDrop(this.ns.EH.getDragTarget(), 0, 0, 0);
        return isc.EH.STOP_BUBBLING;
    }

    if (!this._lastDragOverItem) {
        isc.logWarn("lastDragOverItem not set, cannot drop", "dragDrop");
        return;
    }

    var item = this._lastDragOverItem,
        dropOffsets = this.getItemTableOffsets(item),
        side = item.dropSide,
        index = item._dragItemIndex,
        insertIndex = this.getItemDropIndex(item, side);

    this._lastDragOverItem = null;
    if (this.shouldPassDropThrough()) {
        this.hideDragLine();
        return;
    }

    if (insertIndex != null && insertIndex >= 0) {

        if (this.parentElement) {
            if (this.parentElement.hideDragLine) this.parentElement.hideDragLine();
            if (this.parentElement.hideDropLine) this.parentElement.hideDropLine();
        }

        // Note that we cache a copy of _rowTable because the modifyFormOnDrop() method may
        // end up invalidating the table layout, and thus clearing _rowTable in the middle of
        // its processing
        var rowTable = this.items._rowTable.duplicate();
        this.modifyFormOnDrop(item, dropOffsets.top, dropOffsets.left, side, rowTable);
    }

    this.hideDragLine();
    return isc.EH.STOP_BUBBLING;
},

itemDrop : function (item, itemIndex, rowNum, colNum, side, callback) {

    var source = item.getDragData();
    // If source is null, this is probably because we are drag-repositioning an existing
    // item within a DynamicForm (or from one DF to another) - the source is the component
    // itself
    if (source == null) {
        source = isc.EH.dragTarget;
        if (isc.isA.FormItemProxyCanvas(source)) {
            this.logInfo("The dragTarget is a FormItemProxyCanvas for " +
                        source.formItem, "editModeDragTarget");
            source = source.formItem;
        }
    }

    if (!item.isA("Palette")) {
        if (isc.EditContext._dragHandle) isc.EditContext._dragHandle.hide();
        var tree = this.editContext.getEditNodeTree(),
            oldParent = tree.getParent(source.editNode),
            oldIndex = tree.getChildren(oldParent).indexOf(source.editNode),
            editNode = source.editNode;

        if (isc.isA.Function(this.itemDropping)) {
            editNode = this.itemDropping(editNode, itemIndex, true);
            if (!editNode) return;
        }

        this.editContext.removeNode(editNode);

        // If we've moved the child component to a slot further down in the same parent,
        // indices will now be off by one because we've just removed it from its old slot
        if (oldParent == this.editNode && itemIndex > oldIndex) itemIndex--;

        var node = this.editContext.addNode(source.editNode, this.editNode, itemIndex);
        if (node && node.liveObject) {
            isc.EditContext.delayCall("selectCanvasOrFormItem", [node.liveObject, true], 200);
        }

        return node;
    } else {
        // We're dealing with a drag of a new item from a component palette
        var paletteNode = item.transferDragData();
        if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];

        // loadData() operates asynchronously, so we'll have to finish the item drop off-thread
        if (paletteNode.loadData && !paletteNode.isLoaded) {
            var thisForm = this;
            paletteNode.loadData(paletteNode, function (loadedNode) {
                loadedNode = loadedNode || paletteNode
                loadedNode.isLoaded = true;
                thisForm.completeItemDrop(loadedNode, itemIndex, rowNum, colNum, side, callback)
                loadedNode.dropped = paletteNode.dropped;
            });
            return;
        }

        this.completeItemDrop(paletteNode, itemIndex, rowNum, colNum, side, callback)
    }
},

completeItemDrop : function (paletteNode, itemIndex, rowNum, colNum, side, callback) {

    var liveObject = paletteNode.liveObject,
        canvasEditNode;
    if (!isc.isA.FormItem(liveObject)) {
        if (isc.isA.Button(liveObject) || isc.isAn.IButton(liveObject)) {
            // Special case - Buttons become ButtonItems
            paletteNode = this.editContext.makeEditNode({
                type: "ButtonItem",
                title: liveObject.title,
                defaults : paletteNode.defaults
            })
        } else if (isc.isA.Canvas(liveObject)) {
            canvasEditNode = paletteNode;
            paletteNode = this.editContext.makeEditNode({type: "CanvasItem"});
            isc.addProperties(paletteNode.defaults, {
                showTitle: false,
                startRow: true,
                endRow: true,
                width: "*",
                colSpan: "*"
            });
        }
    }
    paletteNode.dropped = true;

    if (isc.isA.Function(this.itemDropping)) {
        paletteNode = this.itemDropping(paletteNode, itemIndex, true);
        if (!paletteNode) return;
    }

    var nodeAdded = this.editContext.addNode(paletteNode, this.editNode, itemIndex);

    if (nodeAdded) {

        isc.EditContext.clearSchemaProperties(nodeAdded);

        if (canvasEditNode) {
            nodeAdded = this.editContext.addNode(canvasEditNode, nodeAdded, 0);


            // FIXME: Need a cleaner factoring here (see also Layout.dropItem())
            if (isc.isA.TabSet(liveObject)) {

                liveObject.delayCall("showAddTabEditor", [], 1000);
            }
        }

        // If we've just dropped a palette node that contained a reference to a dataSource,
        // do a forced set of that dataSource on the liveObject.  This will take it through
        // any special editMode steps - for example, it will cause a DynamicForm to have a
        // set of fields generated for it and added to the project tree
        if (nodeAdded.liveObject.dataSource) {
            //this.logWarn("calling setDataSource on: " + nodeAdded.liveObject);
            nodeAdded.liveObject.setDataSource(nodeAdded.liveObject.dataSource, null, true);
        }

        isc.EditContext.delayCall("selectCanvasOrFormItem", [paletteNode.liveObject, true], 200);

        if (nodeAdded.showTitle != false) {
            paletteNode.liveObject.delayCall("editClick");
        }
    }
    if (callback) this.fireCallback(callback, "node", [nodeAdded]);

},

// Modifies the form to accommodate the pending drop by adding columns and/or SpacerItems as
// necessary, then performs the actual drop
modifyFormOnDrop : function (item, rowNum, colNum, side, rowTable) {
    if (this.canAddColumns == false) return;

    var dropItem = this.ns.EH.getDragTarget().getDragData(),
    dropItemCols,
    draggingFromRow,
    draggingFromIndex,
    _this = this;

    if (!dropItem) {
        // We're drag-positioning an existing item
        dropItem = this.ns.EH.getDragTarget();
        if (!isc.isA.FormItemProxyCanvas(dropItem)) {
            this.logWarn("In modifyFormOnDrop the drag target was not a FormItemProxyCanvas");
            return;
        }
        dropItem = dropItem.formItem;
        var lastIndex = -1;
        // If the item we're dragging is in this form, note its location so that we can clean
        // up where it came from
        for (var i = 0; i < rowTable.length; i++) {
            for (var j = 0; j < rowTable[i].length; j++) {
                if (rowTable[i][j] == lastIndex) continue;
                lastIndex = rowTable[i][j];
                if (this.items[lastIndex] == dropItem) {
                    draggingFromRow = i;
                    draggingFromIndex = lastIndex;
                    break;
                }
            }
        }
        var dragPositioning = true;
    } else {
        // Manually create a FormItem using the config that will be used to create the real
        // object.  We need to do this because we need to know things about that object that
        // can only be easily discovered by creating and then inspecting it - eg, colSpan,
        // title attributes and whether startRow or endRow are set
        if (isc.isAn.Array(dropItem)) dropItem = dropItem[0];
        var type = dropItem.type || dropItem.className;
        var theClass = isc.ClassFactory.getClass(type);
        if (isc.isA.FormItem(theClass)) {
            dropItem = this.createItem(dropItem, type);
        } else {
            // This is not completely accurate, but it gives us enough info for placement and
            // column occupancy calculation.  dropItem() differentiates between Buttons and
            // other types of Canvas, but for our purposes here it's enough to know that non-
            // FormItem items will occupy one cell and don't have endRow/startRow set
            dropItem = this.createItem({type: "CanvasItem", showTitle: false}, "CanvasItem");
        }
        var dragPositioning = false;
    }

    dropItemCols = this.getAdjustedColSpan(dropItem);

    // If we've previously set startRow or endRow on the item we're dropping, clear them
    if ((dropItem.startRow && dropItem._startRowSetByBuilder) ||
        (dropItem.endRow && dropItem._endRowSetByBuilder)) {
        dropItem.editContext.setNodeProperties(dropItem.editNode, {
            startRow: null,
            _startRowSetByBuilder: null,
            endRow: null,
            _endRowSetByBuilder: null
        });
    }

    // If we're in drag-reposition mode and the rowNum we're dropping on is not the row we're
    // dragging from, we could end up with a situation where a row contains nothing but spacers.
    // Detect when this situation is about to arise and mark the spacers for later deletion
    var spacersToDelete = [];
    if (dragPositioning && draggingFromRow) {
        var fromRow = rowTable[draggingFromRow],
            lastIndex = -1;
        for (var i = 0; i < fromRow.length; i++) {
            if (fromRow[i] != lastIndex) {
                lastIndex = fromRow[i];
                if (this.items[lastIndex] == dropItem) continue;
                if (isc.isA.SpacerItem(this.items[lastIndex]) &&
                    this.items[lastIndex]._generatedByBuilder)
                {
                    this.logDebug("Marking spacer " + this.items[lastIndex].name + " for removal",
                                  "formItemDragDrop");
                    spacersToDelete.add(this.items[lastIndex]);
                    continue;
                }
                this.logDebug("Found a non-spacer item on row " + draggingFromRow +
                              ", no spacers will be deleted", "formItemDragDrop");
                spacersToDelete = null;
                break;
            }
        }
    }

    var delta = 0;

    if (side == "L" || side == "R") {

        var addColumns = true;
        // If the item is flagged startRow: true, we don't need to add columns
        if (dropItem.startRow) addColumns = false;
        // If the item is flagged endRow: true and we're not dropping in the rightmost
        // column, we don't need to add columns (NOTE: this isn't strictly true, we need
        // to revisit this to cope with the case of an item with a larger colSpan than
        // the number of columns remaining to the right)
        if (dropItem.endRow && (side == "L" || colNum < rowTable[rowNum].length)) {
            addColumns = false;
        }
        // If we're repositioning an item and it came from this row in this form, we don't
        // need to add columns
        if (dragPositioning && draggingFromRow == rowNum) addColumns = false;

        // Need to add column(s) and move the existing items around accordingly
        if (addColumns) {
            var cols = dropItemCols;

            // If we're dropping onto a SpacerItem that we created in the first place, we only
            // need to add columns if the colSpan of the dropped item is greater than the
            // colSpan of the spacer (FIXME: and any adjacent spacers)
            var insertIndex = rowTable[rowNum][colNum];
            //if (side == "R") insertIndex++;
            if (rowTable[rowNum].contains(insertIndex)) {
                var existingItem = this.items[insertIndex];

                // If the item being dropped upon is not a spacer, check the item immediately
                // adjacent on the side of the drop
                if (!isc.isA.SpacerItem(existingItem) || !existingItem._generatedByBuilder) {
                    insertIndex += side =="L" ? -1 : 1;
                    existingItem = this.items[insertIndex];
                }

                if (rowTable[rowNum].contains(insertIndex)) {

                    if (isc.isA.SpacerItem(existingItem) && existingItem._generatedByBuilder) {
                        if (existingItem.colSpan && existingItem.colSpan > cols) {
                            existingItem.editContext.setNodeProperties(existingItem.editNode,
                                            {colSpan: existingItem.colSpan - cols});
                            cols = 0;
                        } else {
                            cols -= existingItem.colSpan;
                            existingItem.editContext.removeNode(existingItem.editNode);
                            if (side == "R") delta = -1;
                        }
                    }
                }
            }

            if (cols <= 0) {
                addColumns = false;

            // If we get this far, we are going to insert "dropItemCols" columns to the form.
            // It may be that the form is already wide enough to accommodate those columns in
            // this particular row (the grid has a ragged right edge because we use endRow and
            // startRow to control row breaking rather than unnecessary spacers)
            } else if (rowTable[rowNum].length + dropItemCols <= this.numCols) {
                addColumns = false;
            } else  {
                // Otherwise widen the entire form
                this.editContext.setNodeProperties(this.editNode, {numCols: this.numCols + cols});
            }
        }

        // We're inserting a whole new column to the "grid" that the user sees.  This may not
        // be the desired action - maybe the user just wanted to insert an extra cell in this
        // row?  Leaving as is for now - prompting the user would make this and everything
        // downstream of it asynchronous
        for (var i = 0; i < rowTable.length; i++) {
            var insertIndex = rowTable[i][colNum];
            if (insertIndex == null) insertIndex = this.items.length;
            else insertIndex += delta + (side == "L" ? 0 : 1);
            if (i != rowNum) {
                if (!addColumns) continue;

                // If we're dragging an item to a row higher up the form, we'll have stepped the
                // delta forward when we inserted the dragged item; when we reach the row it
                // used to be on, we need to retard the delta by one to get the insert index
                // back in line
                if (dragPositioning && draggingFromRow &&
                    rowNum < draggingFromRow && i == draggingFromRow)
                {
                    delta--;
                }

                // If spacersToDelete contains anything, we detected up front that this drop-
                // reposition will leave the from row empty of everything except spacer items
                // that we added in the first place.  Those spacers are marked for deletion at
                // the end of this process; we certainly don't want to add any more!
                if (spacersToDelete && spacersToDelete.length > 0 && i == draggingFromRow) {
                    continue;
                }
                // Look to see if the new column is to the right of an item with endRow: true,
                // because in that circumstance the spacer will break the layout
                if (insertIndex > 0) {
                    var existingItem = this.items[insertIndex - 1];
                    if (!existingItem || existingItem == dropItem || existingItem.endRow) {
                        continue;
                    }
                }
                // If the column just added is the rightmost one, we should retain form
                // coherence by marking the right-hand item on each row as endRow: true instead
                // of creating unnecessary spacers
                var existingItemCols = this.getAdjustedColSpan(existingItem);
                if (side == "R" && colNum + existingItemCols >= rowTable[i].length) {
                    if (!existingItem.endRow) {
                        existingItem.editContext.setNodeProperties(existingItem.editNode,
                                    {endRow: true, _endRowSetByBuilder: true});
                    }
                    continue;
                }

                var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
                isc.addProperties(paletteNode.defaults, {
                    colSpan: cols,
                    height: 0,
                    _generatedByBuilder: true
                });
                var nodeAdded = this.editContext.addNode(paletteNode, this.editNode,
                                                         insertIndex);
                // Keep track of how many new items we've added to the form, because we need
                // to step the insert point on for any later adds
                delta++;
            } else {
                if (side == "L") {
                    // We're dropping to the left of an item, so we know there is an item to
                    // our right.  If it specifies startRow, clear that out
                    var existingItem = this.items[insertIndex];
                    if (existingItem && existingItem.startRow && existingItem._startRowSetByBuilder) {
                        existingItem.editContext.setNodeProperties(existingItem.editNode,
                            {startRow: null, _startRowSetByBuilder: null});
                    }
                } else {
                    // We're dropping to the right of an item, so we know there is an item to
                    // our left.  If it specifies endRow, clear that out
                    var existingItem = this.items[insertIndex - 1];
                    if (existingItem && existingItem.endRow && existingItem._endRowSetByBuilder) {
                        existingItem.editContext.setNodeProperties(existingItem.editNode,
                            {endRow: null, _endRowSetByBuilder: null});
                    }
                }

                this.itemDrop(this.ns.EH.getDragTarget(), insertIndex, i, colNum, side,
                    function (node) {
                        _this._nodeToSelect = node;
                    });
                if (draggingFromRow == null || rowNum < draggingFromRow) delta++;
            }
        }
    } else {  // side was "T" or "B"
        var row,
            currentItemIndex;
        // We don't want to drop "above" or "below" a spacer we put in place; we want to
        // replace it
        if (isc.isA.SpacerItem(item) && item._generatedByBuilder) {
            row = rowNum;
        } else {
            row = rowNum + (side == "B" ? 1 : 0);
        }
        if (rowTable[row]) currentItemIndex = rowTable[row][colNum];

        var rowStartIndex;
        if (row >= rowTable.length) rowStartIndex = this.items.length;
        else rowStartIndex = rowTable[row][0];

        var currentItem = currentItemIndex == null ? null : this.items[currentItemIndex];
        if (currentItem == null ||
                (isc.isA.SpacerItem(currentItem) && currentItem._generatedByBuilder)) {
            if (row > rowTable.length - 1 || row < 0) {
                // Dropping past the end or before the beginning of the form - in both cases
                // rowStartIndex will already have been set correctly, so we can just go
                // ahead and add the component, plus any spacers we need
                if (colNum != 0 && !dropItem.startRow) {
                    var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
                    isc.addProperties(paletteNode.defaults, {
                        colSpan: colNum,
                        height: 0,
                        _generatedByBuilder : true
                    });
                    this.editContext.addNode(paletteNode, this.editNode, rowStartIndex);
                }
                this.itemDrop(this.ns.EH.getDragTarget(),
                                rowStartIndex + (colNum != 0 ? 1 : 0), row, colNum, side,
                                function (node) {
                                    _this._nodeToSelect = node;
                                });
                // We have just created an empty line for this item, so we know for sure that
                // it is the only item on the line (except for any spacers we created).
                // Therefore, we mark it endRow: true
            } else if (currentItem == null) {
                // This can only happen if we're dropping on an existing row to the right of
                // a component that specifies endRow: true, or where the first item in the
                // next row specifies startRow: true.  If the reason is a trailing startRow,
                // that's fine and we don't need to do anything special.  If the reason is a
                // leading endRow, that presents a problem.  For now, we assume that the
                // endRow was set by VB, and just change it to suit ourselves.  This will
                // change so that we look to see whether the startRow/endRow attr was set by
                // VB or the user.  If it was set by VB, we just can it as now; if it was set
                // by the user we attempt to honor that by inserting a whole new row and
                // padding on the left, such that the item is dropped immediately above or
                // below the item hilited by the dropline, and the item that specified endRow
                // remains as the last item in its row.
                var leftCol = rowTable[row].length - 1;
                if (leftCol < 0) {
                    isc.logWarn("Found completely empty row in DynamicForm at position (" +
                                    row + "," + (colNum) + ")");
                    return;
                }
                var existingItemIndex = rowTable[row][leftCol];
                var existingItem = this.items[existingItemIndex];
                if (existingItem == null) {
                    isc.logWarn("Null item in DynamicForm at position (" + row + "," + (colNum-1) + ")");
                    return;
                }
                // Special case - don't remove the endRow flag from the existing item if the
                // existing item is also the item we're dropping (as would be the case if the
                // if the user piacks up a field and drops it further to the right in the
                // same column)
                if (existingItem.endRow && existingItem != dropItem) {
                    existingItem.editContext.setNodeProperties(existingItem.editNode, {endRow: false});
                }
                var padding = (colNum - leftCol) - 1;
                // Special case - the item to our left is actually the item we're dropping,
                // so we need to replace it with a spacer or the drop won't appear to have
                // have had any effect
                if (dragPositioning && existingItem == dropItem) {
                    padding += dropItemCols;
                }
                if (padding > 0) {
                    var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
                    isc.addProperties(paletteNode.defaults, {
                        colSpan: padding,
                        height: 0,
                        _generatedByBuilder: true
                    });
                    this.editContext.addNode(paletteNode, this.editNode, existingItemIndex + 1);
                }
                this.itemDrop(this.ns.EH.getDragTarget(),
                                existingItemIndex + (padding > 0 ? 2 : 1), row, colNum, side,
                                function (node) {
                                    _this._nodeToSelect = node;
                                });
            } else {
                // Where the user wants to drop there is currently a SpacerItem that we created
                // to maintain form coherence.  So we do the following:
                // - If the item being dropped is narrower than the spacer, we adjust the
                //   spacer's colSpan accordingly and drop the item in before it
                // - If the item and the spacer are the same width, we remove the spacer and
                //   insert the item in its old position
                // - If the item is wider than the spacer then for now we just replace the
                //   spacer with the item, like we would if they were the same width.  This
                //   may well cause the form to reflow in an ugly way.  To fix this, we will
                //   change this code to look for other spacers in the target row, and
                //   attempt to remove them to make space for the item; if all else fails, we
                //   must add columns to the form and fix up as required to ensure that we
                //   don't get any reflows that break the form's coherence

                var oldColSpan = currentItem.colSpan ? currentItem.colSpan : 1,
                    newColSpan = dropItemCols;
                if (oldColSpan > newColSpan) {
                    currentItem.editContext.setNodeProperties(currentItem.editNode,
                                    {colSpan: oldColSpan - newColSpan});
                    this.itemDrop(this.ns.EH.getDragTarget(), currentItemIndex, row,
                                  colNum, side,
                                  function (node) {
                                      _this._nodeToSelect = node;
                                  });
                } else {
                    this.itemDrop(this.ns.EH.getDragTarget(), currentItemIndex, row,
                                  colNum, side,
                                  function (node) {
                                      _this._nodeToSelect = node;
                                  });
                    currentItem.editContext.removeNode(currentItem.editNode);
                }
            }
        } else {
            // Something is in the way.  We could either insert an entire new row or just push
            // the contents of this one column down a row.  Both of these seem like valid use
            // cases; for now, we're just going with inserting a whole new row
            if (colNum != 0) {
                var paletteNode = this.editContext.makeEditNode({type: "SpacerItem"});
                isc.addProperties(paletteNode.defaults, {
                    colSpan: colNum,
                    height: 0,
                    _generatedByBuilder : true
                });
                this.editContext.addNode(paletteNode, this.editNode, rowStartIndex);
            }
            this.itemDrop(this.ns.EH.getDragTarget(), rowStartIndex + (colNum == 0 ? 0 : 1),
                row, colNum, side, function (node) {
                    if (node && node.liveObject && node.liveObject.editContext) {
                        node.liveObject.editContext.setNodeProperties(node,
                                    {endRow: true, _endRowSetByBuilder: true});
                    }
                    _this._nodeToSelect = node;
                });
        }
    }

    if (dragPositioning && spacersToDelete) {
        for (var i = 0; i < spacersToDelete.length; i++) {
            this.logDebug("Removing spacer item " + spacersToDelete[i].name, "formItemDragDrop");
            spacersToDelete[i].editContext.removeNode(spacersToDelete[i].editNode);
        }
    }

    if (!dragPositioning) dropItem.destroy();

    if (this._nodeToSelect && this._nodeToSelect.liveObject) {
        isc.EditContext.delayCall("selectCanvasOrFormItem", [this._nodeToSelect.liveObject], 200);
    }

},

getAdjustedColSpan  : function(item) {
    if (!item) return 0;
    var cols = item.colSpan != null ? item.colSpan : 1;
    // colSpan of "*" makes no sense for the purposes of this calculation, which is trying to
    // work out how many columns an item we're dropping needs to take up.  So we'll call it 1.
    if (cols == "*") cols = 1;
    if (item.showTitle != false && (item.titleOrientation == "left" ||
                                    item.titleOrientation == "right" ||
                                    item.titleOrientation == null))
    {
        cols++
    }

    return cols;
},

// Override of Canvas.canAdd - DynamicForm will accept a drop of a Canvas in addition to the
// FormItems advertised in its schema
canAdd : function (type) {
    if (this.getObjectField(type) != null) return true;
    var classObject = isc.ClassFactory.getClass(type);
    if (classObject && classObject.isA("Canvas")) return true;
    return false;
},

setEditorType : function (item, editorType) {

    if (!item.editContext) return;

    var tree = item.editContext.getEditNodeTree(),
        parent = tree.getParent(item.editNode),
        index = tree.getChildren(parent).indexOf(item.editNode),
        ctx = item.editContext,
        paletteNode = { type: editorType, defaults: item.editNode.defaults },
        editNode = ctx.makeEditNode(paletteNode);

    ctx.removeNode(item.editNode);
    ctx.addNode(editNode, parent, index);
},

// This undocumented method is called from DF.itemDrop() just before the editNode is
// inserted into the editContext.  This function should return the editNode to actually
// insert - either the passed node if no change is required, or some new value.  Note that
// the "isAdded" parameter will be false if the item was dropped after being dragged from
// elsewhere, as opposed to a drop of a new item from a component palette
itemDropping : function (editNode, insertIndex, isAdded) {

    var item = editNode.liveObject,
        schemaInfo = isc.EditContext.getSchemaInfo(editNode);

    // Case 0: there is no schema information to compare, so nothing to do
    if (!schemaInfo.dataSource) return editNode;

    // Case 1: this is an unbound (so presumably empty) form.  Bind it to the top-level
    // schema associated with this item
    if (!this.dataSource) {
        this.setDataSource(schemaInfo.dataSource);
        this.serviceNamespace = schemaInfo.serviceNamespace;
        this.serviceName = schemaInfo.serviceName;
        return editNode;
    }

    // Case 2: this form is already bound to the top-level schema associated with this item,
    // so we don't need to do anything
    if (schemaInfo.dataSource == isc.DataSource.getDataSource(this.dataSource).ID &&
        schemaInfo.serviceNamespace == this.serviceNamespace &&
        schemaInfo.serviceName == this.serviceName) {
        return editNode;
    }

    // Case 3: this form is already bound to some other schema.  We need to wrap this item
    // in its own sub-form
    var canvasItemNode = this.editContext.makeEditNode({
        type: "CanvasItem",
        defaults: {
            cellStyle: "nestedFormContainer"
        }
    });
    isc.addProperties(canvasItemNode.defaults, {showTitle: false, colSpan: 2});
    canvasItemNode.dropped = true;
    this.editContext.addNode(canvasItemNode, this.editNode, insertIndex);

    var dfNode = this.editContext.makeEditNode({
        type: "DynamicForm",
        defaults: {
            numCols: 2,
            canDropItems: false,
            dataSource: schemaInfo.dataSource,
            serviceNamespace: schemaInfo.serviceNamespace,
            serviceName: schemaInfo.serviceName,
            doNotUseDefaultBinding: true
        }
    });
    dfNode.dropped = true;
    this.editContext.addNode(dfNode, canvasItemNode, 0);

    var nodeAdded = this.editContext.addNode(editNode, dfNode, 0);
    isc.EditContext.clearSchemaProperties(nodeAdded);

},

getFieldEditNode : function (field, dataSource) {
    var editorType = this.getEditorType(field);
    editorType = editorType.substring(0,1).toUpperCase() + editorType.substring(1) + "Item";

    var editNode = {
        type: editorType,
        autoGen: true,
        defaults: {
            name: field.name,
            title: field.title || dataSource.getAutoTitle(field.name)
        }
    }

    return editNode;
}


});

// Edit Mode extras for FormItem and its children
// -------------------------------------------------------------------------------------------

isc.FormItem.addMethods({

// Note: this impl contains code duplicated from Canvas.setEditMode because FormItem does not
// extend Canvas.
setEditMode : function(editingOn, editContext, editNode) {

        if (editingOn == null) editingOn = true;
        if (this.editingOn == editingOn) return;
        this.editingOn = editingOn;

        if (this.editingOn) {
            this.editContext = editContext;
        }

        this.editNode = editNode;

        // If we're going into edit mode, re-route various methods
        if (this.editingOn) {
            this.saveToOriginalValues(["click", "doubleClick", "changed"]);
            this.setProperties({
                click: this.editModeClick,
                doubleClick: this.editModeDoubleClick,
                changed: this.editModeChanged
            });
        } else {
            this.restoreFromOriginalValues(["click", "doubleClick", "changed"]);
        }
},

editModeChanged : function (form, item, value) {
    this.editContext.setNodeProperties(this.editNode, {defaultValue: value});
},

setEditorType : function (editorType) {
    if (this.form) this.form.setEditorType(this, editorType);
}
});

isc.ButtonItem.addMethods({

    editClick : function () {
        var left = this.canvas.getPageLeft(),
            width = this.canvas.getVisibleWidth(),
            top = this.canvas.getPageTop(),
            height = this.canvas.getHeight();

        isc.EditContext.manageTitleEditor(this, left, width, top, height);
    }
});

}

// Edit Mode impl for SectionStack
// -------------------------------------------------------------------------------------------

isc.SectionStack.addMethods({

canAdd : function (type) {
    // SectionStack is a special case for DnD - although it is a VLayout, its schema marks
    // children, peers and members as inapplicable.  However, anything can be put into a
    // SectionStackSection.  Therefore, we accept drop of any canvas, and handle adding it
    // to the appropriate section in the drop method.
    // We also accept a drop of a FormItem; this will be detected downstream and handled by
    // wrapping the FormItem inside an auto-created DynamicForm.  Similarly a DrawItem
    // can be accepted because it will be wrapped inside an auto-created DrawPane.
    if (type == "SectionStackSection") return true;
    var classObject = isc.ClassFactory.getClass(type);
    if (classObject &&
        (classObject.isA("Canvas") || classObject.isA("FormItem") || classObject.isA("DrawItem")))
    {
        return true;
    }
    return false;
},

// Return the modified editNode (or a completely different one); return false to abandon
// the drop
modifyEditNode : function (paletteNode, newEditNode, dropType) {
    if (dropType == "SectionStackSection") return newEditNode;
    var dropPosition = this.getDropPosition();
    if (dropPosition == 0) {
        isc.warn("Cannot drop before the first section header");
        return false;
    }

    var headers = this._getHeaderPositions();
    for (var i = headers.length-1; i >= 0; i--) {
        if (dropPosition > headers[i]) {
            // Return the edit node off the section header
            return this.getSectionHeader(i).editNode;
        }
    }
    // Shouldn't ever get here
    return newEditNode;
},

// getEditModeDropPosition() - explicitly called from getDropPosition if the user isn't doing
// a drag reorder of sections.
getEditModeDropPosition : function (dropType) {
    var pos = this.invokeSuper(isc.SectionStack, "getDropPosition");
    if (!dropType || dropType == "SectionStackSection") {
        return pos;
    }

    var headers = this._getHeaderPositions();
    for (var i = headers.length-1; i >= 0; i--) {
        if (pos > headers[i]) {
            return pos - headers[i] - 1;
        }
    }

    return 0;
},

_getHeaderPositions : function () {
    var headers = [],
        j = 0;
    for (var i = 0; i < this.getMembers().length; i++) {
        if (this.getMember(i).isA(this.sectionHeaderClass)) {
            headers[j++] = i;
        }
    }
    return headers;
}


});


// Edit Mode impl for ListGrid
// -------------------------------------------------------------------------------------------
if (isc.ListGrid != null) {

isc.ListGrid.addMethods({

setEditMode : function(editingOn, editContext, editNode) {

    if (editingOn == null) editingOn = true;
    if (editingOn == this.editingOn) return;

    this.invokeSuper(isc.ListGrid, "setEditMode", editingOn, editContext, editNode);

    if (this.editingOn) {
        this.saveToOriginalValues(["setNoDropIndicator", "clearNoDropIndicator",
                                   "headerClick"]);
        this.setProperties({
            setNoDropIndicator: this.editModeSetNoDropIndicator,
            clearNoDropIndicator: this.editModeClearNoDropIndicator,
            headerClick: this.editModeHeaderClick
        });
    } else {
        // If we're coming out of edit mode, revert to whatever we saved
        this.restoreFromOriginalValues(["setNoDropIndicator", "clearNoDropIndicator",
                                        "headerClick"]);
    }
},

// Canvas.clearNoDropindicator no-ops if the internal _noDropIndicator flag is null.  This
// isn't good enough in edit mode because a canvas can be dragged over whilst the no-drop
// cursor is showing, and we want to revert to a droppable cursor regardless of whether
// _noDropIndicatorSet has been set on this particular canvas.
editModeClearNoDropIndicator : function (type) {
    this.Super("clearNoDropIndicator", arguments);
    this.body.editModeClearNoDropIndicator();
},

// Special editMode version of setNoDropCursor - again, because the base version no-ops in
// circumstances where we need it to refresh the cursor.
editModeSetNoDropIndicator : function () {
    this.Super("setNoDropIndicator", arguments);
    this.body.editModeSetNoDropIndicator();
},

editModeHeaderClick : function (fieldNum) {
    // Select the corresponding ListGridField
    var tree = this.editContext.getEditNodeTree(),
        children = tree.getChildren(tree.findById(this.ID)),
        field = this.getField(fieldNum),
        node
    ;
    // Note that a non-field object could be a child
    // of the ListGrid node so we cannot just index
    // into the child array by the fieldNum.
    for (var i = 0; i < children.length; i++) {
        var child = children[i];
        if (child.name == field.name) {
            node = child;
            break;
        }
    }

    if (node) {
        node.liveObject._visualProxy = this.header.getButton(fieldNum);
        isc.EditContext.selectCanvasOrFormItem(node.liveObject);
    }

    this._headerClickFired = true;
    return isc.EH.STOP_BUBBLING;
},

// HACK: We ideally want a header click to stop event bubbling at that point, but it seems
// that returning STOP_BUBBLING from the headerClick() method does not prevent the ListGrid's
// click event from firing, so the object selection is superseded.  To work around this, we
// maintain a flag on the LG that headerClick has been fired, which this click() impl tests
// and then clears
editModeClick : function () {
    if (this.editNode) {
        if (this._headerClickFired) delete this._headerClickFired;
        else isc.EditContext.selectCanvasOrFormItem(this, true);
        return isc.EH.STOP_BUBBLING;
    }
}


});

}

// Edit Mode impl for TreeGrid
// -------------------------------------------------------------------------------------------
if (isc.TreeGrid != null) {

isc.TreeGrid.addMethods({

    setEditMode : function(editingOn, editContext, editNode) {

        if (editingOn == null) editingOn = true;
        if (editingOn == this.editingOn) return;

        this.invokeSuper(isc.TreeGrid, "setEditMode", editingOn, editContext, editNode);

        if (this.editingOn) {
            // If the TG was not databound and had an empty fieldset at initWidget time, it
            // will have created a default treeField which now appears in its fields property
            // as if it were put there by user code.  We need to detect this circumstance and
            // create a TreeGridField node in the projectComponents tree so the user can
            // manipulate this auto-generated field
            this.editModeCreateDefaultTreeFieldEditNode();
            this.saveToOriginalValues(["addField"]);
            this.setProperties({
                addField: this.editModeAddField
            });
        } else {
            // If we're coming out of edit mode, revert to whatever we saved
            this.restoreFromOriginalValues(["addField"]);
        }
    },

    editModeCreateDefaultTreeFieldEditNode : function () {

        // If we're loading a view, the default nodeTitle is going to be destroyed before the
        // user sees it, so just bail
        if (isc._loadingNodeTree) return;

        // If this TG is databound, we presumably haven't created a default nodeTitle; this
        // being the case, let's bail now so that we don't remove a real user field just
        // because it happens to be called "nodeTitle"
        if (this.dataSource) return;

        var fields = this.fields;
        for (var i = 0; i < fields.length; i++) {
            if (fields[i].name == "nodeTitle") {
                var config = {
                    type: "TreeGridField",
                    autoGen: true,
                    defaults: {
                        name: fields[i].name,
                        title: fields[i].title
                    }
                };
                var editNode = this.editContext.makeEditNode(config);
                this.editContext.addNode(editNode, this.editNode, null, null, true);
                return;
            }
        }
    },

    // Overriding the DBC implementation because we need to treat the field being added as a
    // special case if it has treeField set - there can only be one treeField, so we must
    // remove the extant one.  This could only really happen during Load View (unless we were
    // to change the default for treeField to true in the component palette), so we will just
    // hand the call on if we're not in loading mode
    editModeAddField : function (field, index) {
        this.Super("addField", arguments);

        if (isc._loadingNodeTree) {
            if (field.treeField) {
                var fields = this.getFields();
                for (var i = 0; i < fields.length; i++) {
                    if (fields[i].name != field.name && fields[i].treeField) {
                        this.removeField(fields[i]);
                        break;
                    }
                }
            }
        }

    },


    // TreeGrid needs a special implementation of this method because binding a TreeGrid really
    // means binding the one field in the DataSource that represents the tree; with other DBC's,
    // we bind all the visible fields
    editModeSetDataSource : function (dataSource, fields, forceRebind) {
        //this.logWarn("editMode setDataSource called" + isc.Log.getStackTrace());

        // _loadingNodeTree is a flag set by Visual Builder - its presence indicates that we are
        // loading a view from disk.  In this case, we do NOT want to perform the special
        // processing in this function, otherwise we'll end up with duplicate components in the
        // componentTree.
        // However, TreeGrid needs special treatment because it auto-creates a treeField if it
        // is not passed a list of fields to use.  Since we'll be adding the fields one at a
        // time during View Load, we start out with no fields, so a default will be created.
        //
        if (isc._loadingNodeTree) {
            this.baseSetDataSource(dataSource, fields);
            return;
        }

        if (dataSource == null) return;
        if (dataSource == this.dataSource && !forceRebind) return;

        var fields = this.getFields();

        // remove just the field currently marked treeField: true - in many use cases, this
        // will be the only field in the TreeGrid anyway
        if (fields) {
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i];
                if (field.treeField) {
                    field.treeField = null;
                    var nodeToRemove = field.editNode;
                    break;
                }
            }
        }

        var existingFields = this.getFields();
        existingFields.remove(field);

        // If this dataSource has a single complex field, use the schema of that field in lieu
        // of the schema that was dropped.
        var schema,
            fields = dataSource.fields;
        if (fields && isc.getKeys(fields).length == 1 &&
            dataSource.fieldIsComplexType(fields[isc.firstKey(fields)].name))
        {
            schema = dataSource.getSchema(fields[isc.firstKey(fields)].type);
        } else {
            schema = dataSource;
        }


        // add one editNode for the single field in the DataSource that is named as the
        // "titleField"; if there is no such field, just use the first

        var fields = schema.getFields(),
            titleFieldName = dataSource.titleField;

        if (!isc.isAn.Array(fields)) fields = isc.getValues(fields);

        for (var ix = 0; ix < fields.length; ix++) {
            if (!this.shouldUseField(fields[ix], dataSource)) continue;
            if (titleFieldName == null || titleFieldName == fields[ix].name) {
                var titleField = fields[ix];
                break;
            }
        }

        if (titleField) existingFields.addAt(titleField, 0);

        this.baseSetDataSource(dataSource, existingFields);

        var fieldConfig = this.getFieldEditNode(titleField, schema);
        fieldConfig.defaults.treeField = true;
        var editNode = this.editContext.makeEditNode(fieldConfig);
        this.editContext.addNode(editNode, this.editNode, 0, null);
        // Deferred node removal to here as it avoids leaving the TG with an empty fieldset,
        // because this situation triggers the creation of a default treeField in various
        // places in the TG code
        if (nodeToRemove) this.editContext.removeNode(nodeToRemove, true);
        //this.logWarn("editMode setDataSource done adding fields");
    }


});

}

// Edit Mode impl for ServiceOperation and ValuesMap.  Both of these are non-visual classes
// that can nevertheless appear in a VB app - kind of like DataSources, but they're added to
// the project as a side effect of adding a web service binding.
// -------------------------------------------------------------------------------------------

var basicSetEditMode = function (editingOn, editContext, editNode) {
        if (editingOn == null) editingOn = true;
        if (this.editingOn == editingOn) return;
        this.editingOn = editingOn;

        if (this.editingOn) this.editContext = editContext;

        this.editNode = editNode;
}

isc.ServiceOperation.addMethods({
    setEditMode : basicSetEditMode,
    getActionTargetTitle : function () {
        return "Operation: [" + this.operationName + "]";
    }
});

if (isc.ValuesManager != null) {
    isc.ValuesManager.addMethods({
        setEditMode : basicSetEditMode
    });
}


// EditNode
// ---------------------------------------------------------------------------------------

//> @object EditNode
// An object representing a component that is currently being edited within an
// +link{EditContext}.
// <P>
// An EditNode is essentially a copy of a +link{PaletteNode}, initially with the same properties
// as the PaletteNode from which it was generated.  However unlike a PaletteNode, an EditNode
// always has a +link{editNode.liveObject,liveObject} - the object created from the
// +link{paletteNode.defaults} or other properties defined on a paletteNode.
// <P>
// Like a Palette, an EditContext may use properties such as +link{paletteNode.icon} or
// +link{paletteNode.title} to display EditNodes.
// <P>
// An EditContext generally offers some means of editing EditNodes and, as edits are made,
// updates +link{editNode.defaults} with the information required to re-create the component.
//
// @inheritsFrom PaletteNode
// @treeLocation Client Reference/Tools
// @visibility external
//<

//> @attr editNode.defaults (Properties : null : IR)
// Properties required to recreate the current +link{editNode.liveObject}.
// @visibility external
//<

//> @attr editNode.type (SCClassName : null : IR)
// +link{SCClassName} of the <smartclient>+link{liveObject}</smartclient>
// <smartgwt>+link{canvasLiveObject}</smartgwt>, for example, "ListGrid".
// @visibility external
//<

//> @attr editNode.liveObject (Object : null : IR)
// Live version of the object created from the +link{editNode.defaults}.  For example,
// if +link{editNode.type} is "ListGrid", <code>liveObject</code> would be a ListGrid.
// @visibility external
//<


//> @attr editNode.editDataSource (DataSource : null : IR)
// DataSource to use when editing the properties of this component.  Defaults to
// +link{editContext.dataSource}, or the DataSource named after the component's type.
//
// @visibility internal
//<


// EditContext
// --------------------------------------------------------------------------------------------

//> @interface EditContext
// An EditContext provides an editing environment for a set of components.
// <P>
// An EditContext is typically populated by adding a series of +link{EditNode,EditNodes} created via a
// +link{Palette}, either via drag and drop creation, or when loading from a saved version,
// via +link{EditContext.addFromPaletteNode(),addFromPaletteNode()} or
// +link{EditContext.addPaletteNodesFromXML(),addPaletteNodesFromXML()}.
// <P>
// An EditContext then provides interfaces for further editing of the components represented
// by EditNodes.
//
// @group devTools
// @treeLocation Client Reference/Tools
// @visibility external
//<
isc.ClassFactory.defineInterface("EditContext");



//> @attr EditContext.editDataSource   (DataSource : null : IR)
// Default DataSource to use when editing any component in this context.  Defaults to the
// DataSource named after the component's type.  Can be overridden per-component via
// +link{editedItem.editDataSource}.
//
// @group devTools
//<

isc.EditContext.addClassProperties({
_dragHandleHeight: 18,
_dragHandleWidth: 18,
_dragHandleXOffset: -18,
_dragHandleYOffset: 0
});

isc.EditContext.addClassMethods({

    // Title Editing (for various components: buttons, tabs, etc)
    // ---------------------------------------------------------------------------------------
    manageTitleEditor : function (targetComponent, left, width, top, height) {
        if (!isc.isA.DynamicForm(this.titleEditor)) {
            this.titleEditor = isc.DynamicForm.create({
                autoDraw: false,
                margin: 0, padding: 0, cellPadding: 0,
                fields: [
                    {
                        name: "title", type: "text",
                        showTitle: false,
                        keyPress : function (item, form, keyName) {
                            if (keyName == "Escape") {
                                form.discardUpdate = true;
                                form.hide();
                                return
                            }
                            if (keyName == "Enter") item.blurItem();
                        },
                        blur : function (form, item) {
                            // WWW this.logWarn("Blurring...");
                            //this.logWarn(this.getStackTrace());
                            if (!form.discardUpdate) {
                                var widget = form.targetComponent,
                                ctx = widget.editContext;
                                if (ctx) {
                                    ctx.setNodeProperties(widget.editNode,
                                                           {"title" : item.getValue()});
                                    ctx.nodeClick(ctx, widget.editNode);
                                }
                            }
                            form.hide();
                        }
                    }
                ]
            });
        }

        var editor = this.titleEditor;
        editor.setProperties({targetComponent: targetComponent});
        editor.discardUpdate = false;

        var item = editor.getItem("title");
        var title = targetComponent.title;
        if (!title) {
            title = targetComponent.name;
        }
        item.setValue(title);

        this.positionTitleEditor(targetComponent, left, width, top, height);

        editor.show();
        item.focusInItem();
        item.delayCall("selectValue", [], 100);
        // WWW this.logWarn("Showing editor...");
    },

    positionTitleEditor : function (targetComponent, left, width, top, height) {
        if (top == null) top = targetComponent.getPageTop();
        if (height == null) height = targetComponent.height;
        if (left == null) left = targetComponent.getPageLeft();
        if (width == null) width = targetComponent.getVisibleWidth();

        var editor = this.titleEditor;
        var item = editor.getItem("title");
        item.setHeight(height);
        item.setWidth(width);

        editor.setTop(top);
        editor.setLeft(left);
    },

    // Selection and Dragging of EditNodes
    // ---------------------------------------------------------------------------------------

    deselect : function () {
        isc.SelectionOutline.deselect();
        this.hideDragHandle();
    },

    setEditMode : function (editingOn) {
        var selectedComponent = isc.SelectionOutline.getSelectedObject();
        if (selectedComponent == null) return;

        if (editingOn) {
            this.setupDragProperties(selectedComponent);
            this.showSelectedObjectDragHandle();
            isc.SelectionOutline.showOutline();
        } else {
            this.resetDragProperties(selectedComponent);
            this.hideDragHandle();
            isc.SelectionOutline.hideOutline();
        }
    },

    // In editMode, we allow dragging the selected canvas using the drag-handle
    // This involves overriding some default behaviors at the widget level.
    setupDragProperties : function (component) {

        if (component.saveToOriginalValues) {
            component.saveToOriginalValues([
                                            "canDrag",
                                            "canDrop",
                                            "dragAppearance",
                                            "dragStart",
                                            "dragMove",
                                            "dragStop",
                                            "setDragTracker"
                                            ]);
        }

        if (component.setProperties) {
            component.setProperties({
                canDrop: true,
                dragAppearance: "outline",
                // These method overrides are to clobber special record-based drag handling
                // implemented by ListGrid and its children
                dragStart : function () { return true; },
                dragMove : function () { return true; },
                setDragTracker : function () {isc.EH.setDragTracker(""); return false; },
                dragStop : function () {
                    isc.EditContext.hideProxyCanvas();
                    isc.EditContext.positionDragHandle();
                }
            });
        }

    },
    resetDragProperties : function (component) {

        if (this.observer) this.observer.ignore(component, "dragMove");
        component.restoreFromOriginalValues([
            "canDrag",
            "canDrop",
            "dragAppearance",
            "dragStart",
            "dragMove",
            "dragStop",
            "setDragTracker"
        ]);
    },

    selectCanvasOrFormItem : function (object, hideLabel) {

        // Make sure we're not being asked to select a non-visual object like a DataSource
        // or ServiceOperation.  We also support the idea of a visual proxy for a non-widget
        // object - for example, ListGridFields are represented visually by the corresponding
        // button in the ListGrid header.
        if (!isc.isA.Canvas(object) && !isc.isA.FormItem(object) && !object._visualProxy) {
            return;
        }
        // Or a Menu (ie, a context menu which has no visibility until an appropriate object
        // is right-clicked by the user)
        if (isc.isA.Menu(object)) {
            return;
        }

        if (this._dragHandle) this._dragHandle.hide();

        var selectedObject = isc.SelectionOutline.getSelectedObject();
        if (selectedObject) this.resetDragProperties(selectedObject);

        var underlyingObject,
            overrideLabel;
        if (object._visualProxy) {
            var type = object.type || object._constructor;
            overrideLabel = "[" + type + " " + (object.name ? "name:" : "ID");
            overrideLabel += object.name || object.ID;
            overrideLabel += "]"
            underlyingObject = object;
            object = object._visualProxy;
        }

        var editContext = underlyingObject ? underlyingObject.editContext : object.editContext;
        if (!editContext) return;

        // If parent component is a H/VLayout or Stack configure the highlight to
        // allow resizing of the component from along the length axis.
        var node = object.editNode,
            parentNode = editContext.getEditNodeTree().getParent(node),
            resizeFrom
        ;
        if (parentNode) {
            var parentLiveObject = parentNode.liveObject;
            if (parentLiveObject) {
                if (isc.isA.Layout(parentLiveObject)) {
                    var vertical = parentLiveObject.vertical,
                        fill = ((vertical ? parentLiveObject.vPolicy : parentLiveObject.hPolicy) == isc.Layout.FILL),
                        childCount = parentLiveObject.getMembers().length,
                        objectIndex = parentLiveObject.getMemberNumber(object),
                        lastMember = (objectIndex == (childCount-1)),
                        canResize = (!fill || !lastMember)
                    ;
                    if (canResize) {
                        resizeFrom = (vertical ? "B" : "R");
                    }
                }
            }
        }

        var vb = editContext.creator;
        isc.SelectionOutline.select(object, false,
                                    !(hideLabel && vb && vb.hideLabelWhenSelecting),
                                    overrideLabel,
                                    resizeFrom);

        // For conceptual objects that needed a visual proxy, now we've done the physical
        // on-screen selection we need to flip the object back to the underlying one
        if (underlyingObject) object = underlyingObject;

        if (object.editingOn) {
            this.setupDragProperties(object);
            this.showSelectedObjectDragHandle();

            var ctx = object.editContext;

            if (ctx.selectRecord) {
                ctx.deselectAllRecords();
                var ctxData = ctx.getEditNodeTree();
                if (isc.isA.Canvas(object)) {
                    // Canvas objects are created with reasonably friendly IDs that appear
                    // as visual identifiers in the component tree
                    // (Except for section header objects, that is...)
                    if (isc.isA.SectionHeader(object) || isc.isA.ImgSectionHeader(object)) {
                        ctx.selectRecord(ctxData.findById(object._ID));
                    } else {
                        ctx.selectRecord(ctxData.findById(object.ID));
                    }
                } else {
                    // FormItems have standard system-assigned IDs like isc_TextItem_1234.
                    // They are identified in the component tree by name.
                    ctx.selectRecord(ctxData.find({ID: object.name}));
                }
            }
            if (ctx.creator && ctx.creator.editComponent) ctx.creator.editComponent(object.editNode, object);
        }
    },

    showSelectedObjectDragHandle : function () {
        if (!this._dragHandle) {
            var _this = this;
            this._dragHandle = isc.Img.create({
                src: "[SKIN]/../../ToolSkin/images/controls/dragHandle.gif",
                prompt:"Grab here to drag component",
                width: this._dragHandleWidth, height: this._dragHandleHeight,
                cursor:"move",
                backgroundColor:"white",
                opacity: 80,
                canDrag: true,
                canDrop: true,
                isMouseTransparent: true,
                mouseDown : function () {
                    // Remember the offset from the top-left corner of the target widget when
                    // a drag starts (OK, this is mouseDown, but all drags start from the
                    // co-ords of the most recent mouseDown, so it works)
                    this.dragIconOffsetX = isc.EH.getX() -
                                              isc.EditContext.draggingObject.getPageLeft();
                    this.dragIconOffsetY = isc.EH.getY() -
                                              isc.EditContext.draggingObject.getPageTop();
                    _this._mouseDown = true;
                    this.Super("mouseDown", arguments);
                },
                mouseUp : function () {
                    _this._mouseDown = false;
                }
            });
        }
        if (this.draggingObject) {
            this.observer.ignore(this.draggingObject, "dragMove");
            this.observer.ignore(this.draggingObject, "dragStop");
            this.observer.ignore(this.draggingObject, "hide");
            this.observer.ignore(this.draggingObject, "destroy");
        }

        var dragTarget = isc.SelectionOutline.getSelectedObject();
        if (isc.isA.FormItem(dragTarget)) {
            // dragTarget must be a canvas, so wrap the formItem in a proxy canvas
            if (!this._dragTargetProxy) {
                this._dragTargetProxy = isc.FormItemProxyCanvas.create();
            }
            this._dragTargetProxy.delayCall("setFormItem", [dragTarget]);
            dragTarget = this._dragTargetProxy;
        }

        this._dragHandle.setProperties({dragTarget: dragTarget});
        isc.Timer.setTimeout("isc.EditContext.positionDragHandle()", 0);

        if (!this.observer) this.observer = isc.Class.create();

        this.draggingObject = dragTarget;
        this.observer.observe(this.draggingObject, "dragMove",
                    "isc.EditContext.positionDragHandle(true)");
        this.observer.observe(this.draggingObject, "dragStop",
                    "isc.EditContext._mouseDown = false");
        this.observer.observe(this.draggingObject, "hide",
                    "isc.EditContext._dragHandle.hide()");
        this.observer.observe(this.draggingObject, "destroy",
                    "isc.EditContext._dragHandle.hide()");
        this._dragHandle.show();
    },

    hideProxyCanvas : function () {
        if (this._dragTargetProxy) this._dragTargetProxy.hide();
    },

    positionDragHandle : function (offset) {
        if (!this._dragHandle) return;

        var selected = this.draggingObject;

        if (selected.destroyed || selected.destroying) {
            this.logWarn("target of dragHandle: " + isc.Log.echo(selected) + " is invalid: " +
                         selected.destroyed ? "already destroyed"
                                            : "currently in destroy()");
            return;
        }

        var height = selected.getVisibleHeight();
        if (height < this._dragHandleHeight * 2) {
            // Center the drag handle next to the item (the -1 makes it look slightly more
            // correct, because the image has a completely white line 1px thick at the top,
            // giving the impression on a white background that it's lower down than it
            // actually is)
            this._dragHandleYOffset = Math.round((height - this._dragHandle.height) / 2) - 1;
        } else {
            // Place the drag handle at the top-left corner of a taller item
            this._dragHandleYOffset = -1;
        }

        if (selected.isA("FormItemProxyCanvas") && !this._mouseDown) {
            selected.syncWithFormItemPosition();
        }
        if (!selected) return;
        var left = selected.getPageLeft() + this._dragHandleXOffset;
        if (offset) {
            left += selected.getOffsetX() - this._dragHandle.dragIconOffsetX;
        }
        this._dragHandle.setPageLeft(left);

        var top = selected.getPageTop() + this._dragHandleYOffset;
        if (offset) {
            top += selected.getOffsetY() - this._dragHandle.dragIconOffsetY;
        }
        this._dragHandle.setPageTop(top);

        this._dragHandle.bringToFront();
    },

    hideDragHandle : function () {
        if (this._dragHandle) this._dragHandle.hide();
    },

    showDragHandle : function () {
        if (this._dragHandle) this._dragHandle.show();
    },

    hideAncestorDragDropLines : function (object) {
        while (object && object.parentElement) {
            if (object.parentElement.hideDragLine) object.parentElement.hideDragLine();
            if (object.parentElement.hideDropLine) object.parentElement.hideDropLine();
            object = object.parentElement;
            if (isc.isA.FormItem(object)) object = object.form;
        }
    },

    getSchemaInfo : function (editNode) {
        var schemaInfo = {},
            liveObject = editNode.liveObject;

        if (!liveObject) return schemaInfo;

        if (isc.isA.FormItem(liveObject)) {
            if (liveObject.form && liveObject.form.dataSource) {
                var form = liveObject.form;
                schemaInfo.dataSource = isc.DataSource.getDataSource(form.dataSource).ID;
                schemaInfo.serviceName = form.serviceName;
                schemaInfo.serviceNamespace = form.serviceNamespace;
            } else {
                schemaInfo.dataSource = liveObject.schemaDataSource;
                schemaInfo.serviceName = liveObject.serviceName;
                schemaInfo.serviceNamespace = liveObject.serviceNamespace;
            }
        } else if (isc.isA.Canvas(liveObject)) {
                schemaInfo.dataSource = isc.DataSource.getDataSource(liveObject.dataSource).ID;
                schemaInfo.serviceName = liveObject.serviceName;
                schemaInfo.serviceNamespace = liveObject.serviceNamespace;
        } else {
            // If it's not a FormItem or a Canvas, then we must presume it's a config object.
            // This can happen on drop of new components
            schemaInfo.dataSource = liveObject.schemaDataSource;
            schemaInfo.serviceName = liveObject.serviceName;
            schemaInfo.serviceNamespace = liveObject.serviceNamespace;
        }

        return schemaInfo;
    },

    clearSchemaProperties : function (node) {
        if (node && node.defaults && isc.isA.FormItem(node.liveObject)) {
            delete node.defaults.schemaDataSource;
            delete node.defaults.serviceName;
            delete node.defaults.serviceNamespace;
            var form = node.liveObject.form;
            if (form && form.inputSchemaDataSource &&
                isc.DataSource.get(form.inputSchemaDataSource).ID == node.defaults.inputSchemaDataSource &&
                form.inputServiceName == node.defaults.inputServiceName &&
                form.inputServiceNamespace == node.defaults.inputServiceNamespace)
            {
                delete node.defaults.inputSchemaDataSource;
                delete node.defaults.inputServiceName;
                delete node.defaults.inputServiceNamespace;
            }
        }
    },

    // XML source code generation
    // ---------------------------------------------------------------------------------------

    // serialize a set of component definitions to XML code, that is, essentially the
    // editNode.defaults portion ( { _constructor:"Something", prop1:value, ... } )
    serializeDefaults : function (defaults) {
        if (defaults == null) return null;

        if (!isc.isAn.Array(defaults)) defaults = [defaults];

        var output = isc.SB.create();

        isc.Comm.omitXSI = true;
        for (var i = 0; i < defaults.length; i++) {
            var obj = defaults[i],
                tagName = obj._tagName,
                schema = isc.DS.getNearestSchema(obj);

            // The tag name outputted by the XML serialization will be tagName, if set.
            // Otherwise it will be the tag name implied by the schema.
            // Note that this effectively reserves the attribute name "_tagName".

            output.append(schema.xmlSerialize(obj, null, null, tagName), "\n\n");
        }
        isc.Comm.omitXSI = null;

        return output.toString();
    },

    convertActions : function (node, defaults, classObj) {
        // Convert actions defined as a raw object to StringMethods so they can be
        // serialized correctly.

        // This is a bit of a pain to achieve as there's nothing in the component's defaults
        // that makes it clear that this is a StringMethod object rather than some other
        // simple object and there are no dataSource field definitions for most stringMethods
        // - We could examine the registered stringMethod for the class, but this wouldn't
        //   work for non instance object fields, such as stringMethods on ListGridFields
        // - We could just examine the object - if it's a valid format (has target, name attrs)
        //   we could assume it's an action - but this would catch false positives in some
        //   cases
        // For now - look at the value on the live-instance and see if it's a function produced
        // from an Action (check for function.iscAction).
        // This will work as long as the live-object has actually been instantiated (may not be
        // a valid test in all cases - EG anything that's lazily created on draw or when called
        // may not yet have converted it to a function).

        for (var field in defaults) {
            var value = defaults[field];
            // if it's not an object or is already a StringMethod no need to convert to one
            if (!isc.isAn.Object(value) || isc.isA.StringMethod(value)) continue;

            // If it has a specified field-type, other than StringMethod - we don't need
            // to convert
            // Note: type Action doesn't need conversion to a StringMethod as when we serialize
            // to XML, the ActionDataSource will do the right thing
            var fieldType;
            if (classObj && classObj.getField) fieldType = classObj.getField(field).type;
            if (fieldType && (fieldType != "StringMethod")) continue;

            var liveValue = node.liveObject[field],
                liveAction = liveValue ? liveValue.iscAction : null,
                convertToSM
            ;
            if (liveAction) convertToSM = true;
            /*
            // We could add a sanity check that the value will convert to a function successfully
            // in case a function has been added since init or something.
            try {
                isc.Func.expressionToFunction("", defaults[field]);
            } catch (e) {
                convertToSM = false;
            }
            */
            if (convertToSM) defaults[field] = isc.StringMethod.create({value:value});
        }
        // no need to return anything we've modified the defaults object directly.
    }
});


isc.EditContext.addInterfaceProperties({
    //> @attr editContext.defaultPalette (Palette : null : IRW)
    // +link{Palette} to use when an +link{EditNode} is being created directly by this EditContext,
    // instead of being created due to a user interaction with a palette (eg dragging from
    // a +link{TreePalette}, or clicking on +link{MenuPalette}).
    // <P>
    // If no defaultPalette is provided, the EditContext uses an automatically created
    // +link{HiddenPalette}.
    //
    // @visibility external
    //<
    // defaultPalette: null,

    //> @method editContext.getDefaultPalette()
    // @include editContext.defaultPalette
    // @return (Palette) the default Palette
    // @visibility external
    //<
    getDefaultPalette : function () {
        if (this.defaultPalette) return this.defaultPalette;
        return (this.defaultPalette = isc.HiddenPalette.create());
    },

    //> @method editContext.setDefaultPalette()
    // @include editContext.defaultPalette
    // @param palette (Palette) the default Palette
    // @visibility external
    //<
    setDefaultPalette : function (palette) {
        this.defaultPalette = palette;

        // If the palette has no defaultEditContext, then set it
        if (palette && !palette.defaultEditContext) {
            palette.defaultEditContext = this;
        }
    },

    //> @attr editContext.extraPalettes (Array of Palette : null : IRW)
    // Additional +link{Palette,Palettes} to consult for metadata when
    // deserializing +link{EditNode,Edit Nodes}. Note that the
    // +link{defaultPalette,defaultPalette} is always consulted and need not be
    // provided again here.
    //
    // @visibility external
    //<
    // extraPalettes: null,

    //> @attr EditContext.persistCoordinates (boolean : true : IRW)
    // If enabled, changes to a +link{EditNode.liveObject,liveObject}'s position
    // and size will be persisted to their +link{EditNode,EditNodes}.  This
    // applies to both programmatic calls and user interaction (drag reposition
    // or drag resize).
    //
    // @visibility external
    //<
    persistCoordinates: true,

    // Finds a palette node in the defaultPalette or other palettes provided
    findPaletteNode : function (fieldName, value) {
        // Try the default palette first
        var paletteNode = this.getDefaultPalette().findPaletteNode(fieldName, value);
        if (paletteNode) return paletteNode;

        if (this.extraPalettes) {
            if (!isc.isAn.Array(this.extraPalettes)) this.extraPalettes = [this.extraPalettes];

            // If not found, try any other palettes provided
            for (var i = 0; i < this.extraPalettes.length; i++) {
                paletteNode = this.extraPalettes[i].findPaletteNode(fieldName, value);
                if (paletteNode) return paletteNode;
            }
        }

        // If not found anywhere, return null
        return null;
    },

    //> @method editContext.addNode()
    // Add a new +link{EditNode} to the EditContext, under the specified parent.
    // <P>
    // The EditContext will interrogate the parent and new nodes to determine what field
    // within the parent allows a child of this type, and to find a method to add the newNode's
    // liveObject to the parentNode's liveObject.  The new relationship will then be stored
    // in the tree of EditNodes.
    // <P>
    // For example, when a Tab is dropped on a TabSet, the field TabSet.tabs is discovered as
    // the correct target field via naming conventions, and the method TabSet.addTab() is likewise
    // discovered as the correct method to add a Tab to a TabSet.
    //
    // @param newNode (EditNode) new node to be added
    // @param [parentNode] (EditNode) parent to add the new node under.
    // @param [index] (integer) index within the parent's children array
    // @param [parentProperty] (string) the property of the liveParent to which the new node should
    //                                  be added, if not auto-discoverable from the schema
    // @param [skipParentComponentAdd] (Boolean) whether to skip adding the liveObject to the liveParent
    //                                           (default false)
    // @return newNode (EditNode) node added
    // @visibility external
    //<
    addNode : function (newNode, parentNode, index, parentProperty, skipParentComponentAdd) {
        var iscClass = isc.ClassFactory.getClass(newNode.type);
        if (iscClass && iscClass.isA(isc.DataSource)) {
            // If we're adding a datasource that must be loaded, then defer the addNode
            // until the datasource is loaded
            if (newNode.loadData && !newNode.isLoaded) {
                var self = this;
                newNode.loadData(newNode, function () {
                    self.addNode(newNode, parentNode, index, parentProperty, skipParentComponentAdd);
                });
                return;
            }
        }

        var data = this.getEditNodeTree();

        var defaultParentNode = this.getDefaultParent(newNode);
        if (parentNode == null) parentNode = defaultParentNode;

        var liveParent = this.getLiveObject(parentNode);
        this.logInfo("addNode will add newNode of type: " + newNode.type +
                     " to: " + this.echoLeaf(liveParent), "editing");

        if (liveParent.wrapChildNode) {
            parentNode = liveParent.wrapChildNode(this, newNode, parentNode, index);
            if (!parentNode) return;
            liveParent = this.getLiveObject(parentNode);
        }

        // find what field in the parent can accommodate a child of this type (prefer the
        // passed-in name over a looked-up one, so the user can override in the case of
        // multiple valid parent fields)
        var fieldName = parentProperty || isc.DS.getObjectField(liveParent, newNode.type);
        var field = isc.DS.getSchemaField(liveParent, fieldName);

        if (!field) {

            if (!iscClass || !iscClass.isA(isc.DataSource) || parentNode != defaultParentNode) {
                this.logWarn("can't addNode: can't find a field in parent: " + liveParent +
                             " for a new child of type: " + newNode.type + ", parent property:" + fieldName +
                             ", newNode is: " +
                             this.echo(newNode));
            }
            return;
        }

        // for a singular field (eg listGrid.dataSource), remove the old node first
        if (!field.multiple) {
            var existingChild = isc.DS.getChildObject(liveParent, newNode.type, parentProperty);
            if (existingChild) {
                var existingChildNode = data.getChildren(parentNode).find("ID", isc.DS.getAutoId(existingChild));
                this.logWarn("destroying existing child: " + this.echoLeaf(existingChild) +
                             " in singular field: " + fieldName);
                data.remove(existingChildNode);
                if (isc.isA.Class(existingChild) && !isc.isA.DataSource(existingChild)) existingChild.destroy();
            }
        }

        // NOTE: generated components and remove/add cycles: some widgets convert config
        // objects into live objects (eg formItem properties to live FormItem, tab -> ImgTab,
        // section -> SectionHeader, etc).  When we are doing an add/remove cycle for these
        // kinds of generated objects:
        // - rebuild based on defaults, rather than trying to re-add the liveObject, which will
        //   be a generated component that the parent will have destroyed
        // - preserve Canvas children of the generated component, such as tab.pane,
        //   section.items, which have not been added to the defaults.  We do this by using
        //   part of the serialization logic (serializeChildData)
        // - ensure removal of the tab, item, or section does not destroy these Canvas children
        //   (a special flag is passed to at least TabSets to avoid this)

        // Optimization for add/remove cycles: check for methods like "reorderMember" first.
        // Note this doesn't remove the complexity discussed above because a generated
        // component might be moved between two parents.
        var childObject;
        if (newNode.generatedType) {
            // copy to avoid property scribbling that is currently done by TabSets and
            // SectionStacks at least
            childObject = isc.addProperties({}, newNode.defaults);
            this.serializeChildData(childObject, data.getChildren(newNode));
        } else {
            childObject = newNode.liveObject;
        }

        // Let the liveObject know about the editContext and editNode. We used
        // to do this for some objects in addedToEditContext, but that isn't
        // sufficient for liveObjects that are in fact config blocks (since they
        // don't have the callback).
        childObject.editContext = this;
        childObject.editNode = newNode;

        if (!skipParentComponentAdd) {
            var result = isc.DS.addChildObject(liveParent, newNode.type, childObject, index, parentProperty);
            if (!result) {
                this.logWarn("addChildObject failed, returning");
                return;
            }
        }

        // fetch the liveObject back from the parent to handle it's possible conversion from
        // just properties to a live instance.
        // NOTE: fetch object by ID, not index, since on a reorder when a node is dropped after
        // itself the index is one too high
        if (!newNode.liveObject) newNode.liveObject = isc.DS.getChildObject(liveParent, newNode.type,
                                                                            isc.DS.getAutoId(newNode.defaults), parentProperty);

        this.logDebug("for new node: " + this.echoLeaf(newNode) +
                      " liveObject is now: " + this.echoLeaf(newNode.liveObject),
                      "editing");

        if (newNode.liveObject == null) {
            this.logWarn("wasn't able to retrieve live object after adding node of type: " +
                         newNode.type + " to liveParent: " + liveParent +
                         ", does liveParent have an appropriate getter() method?");
        }

        // add the node representing the component to the project tree
        data.add(newNode, parentNode, index);
        // gets rid of the spurious opener icon that appears because all nodes are regarded as
        // folders and dropped node is unloaded, hence might have children
        data.openFolder(newNode);

        this.logInfo("added node " + this.echoLeaf(newNode) +
                     " to EditTree at path: " + data.getPath(newNode) +
                     " with live object: " + this.echoLeaf(newNode.liveObject), "editing");

        // Call hook in case the EditContext wants to do further processing ... useful to avoid
        // problem with calling Super with an interface method
        if (this.nodeAdded) this.nodeAdded(newNode);

        // Call hook in case the live object wants to know about being added
        if (newNode.liveObject.addedToEditContext) newNode.liveObject.addedToEditContext(this, newNode, parentNode, index);

        return newNode;
    },

    //>!BackCompat 2011.06.25
    addComponent : function (newNode, parentNode, index, parentProperty, skipParentComponentAdd) {
        return this.addNode(newNode, parentNode, index, parentProperty, skipParentComponentAdd);
    },
    //<!BackCompat

    //> @method editContext.reorderNode()
    // Moves an +link{EditNode} from one child index to another in the EditContext under the specified parent.
    // <P>
    // No changes are made to the live objects.
    //
    // @param parentNode (EditNode) parent to reorder child nodes
    // @param index (integer) index within the parent's children array to be moved
    // @param moveToIndex (integer) index within the parent's children array at which to place moved node
    // @visibility devTools
    //<
    reorderNode : function (parentNode, index, moveToIndex) {
        var data = this.getEditNodeTree();

        // Locate child node that has moved
        var childNode = data.getChildren(parentNode).get(index);

        // Remove the child node from the tree and insert it back at the new location
        data.remove(childNode);
        data.add(childNode, parentNode, moveToIndex);
    },

    //> @method editContext.nodeAdded()
    // Notification fired when an +link{EditNode} has been added to the EditContext
    //
    // @param newNode (EditNode) node that was added
    // @visibility external
    //<
    // Empty function in case someone wants to observe.
    nodeAdded : function (newNode) {},

    getDefaultParent : isc.ClassFactory.TARGET_IMPLEMENTS,

    //> @method editContext.addFromPaletteNode()
    // Creates a new EditNode from a PaletteNode, using the
    // +link{defaultPalette}.  If you have an array of possibly inter-related
    // PaletteNodes, then you should use
    // +link{addFromPaletteNodes(),addFromPaletteNodes()} on the array instead,
    // in order to preserve the relationships.
    //
    // @param paletteNode (PaletteNode) the palette node to use to create the new node
    // @param [parentNode] (EditNode) optional the parent node if the new node should appear
    //                                under a specific parent
    // @return (EditNode) the EditNode created from the paletteNode
    // @see addFromPaletteNodes()
    // @visibility external
    //<
    addFromPaletteNode : function (paletteNode, parentNode) {

        var editNode = this.makeEditNode(paletteNode, parentNode);
        return this.addNode(editNode, parentNode);
    },

    //> @method editContext.makeEditNode()
    // Creates and returns an EditNode using the +link{defaultPalette}.  Does not add the newly
    // created EditNode to the EditContext.
    //
    // @param paletteNode (PaletteNode) the palette node to use to create the new node
    // @return (EditNode) the EditNode created from the paletteNode
    // @visibility external
    //<
    makeEditNode : function (paletteNode) {
        var palette = this.getDefaultPalette();
        return palette.makeEditNode(paletteNode);
    },

    // alternative to just using node.liveObject
    // exists because forms used to rebuild *all* items when any single item is added, hence
    // making the liveObject stale for siblings of an added item
    getLiveObject : function (node) {
        var data = this.getEditNodeTree();
        var parentNode = data.getParent(node);

        // at root, just use the cached liveObject (a formItem can never be at root)
        if (parentNode == null) return node.liveObject;


        var liveParent = parentNode.liveObject;
        var liveObject = isc.DS.getChildObject(liveParent, node.type, isc.DS.getAutoId(node));

        if (liveObject) node.liveObject = liveObject;
        return node.liveObject;
    },

    // wizard handling
    requestLiveObject : function (newNode, callback, palette) {
        var _this = this;

        // handle deferred nodes (don't load or create their liveObject until they are actually
        // added).  NOTE: arguably the palette should handle this, and makeEditNode()
        // should be asynchronous in this case.
        if (newNode.loadData && !newNode.isLoaded) {
            newNode.loadData(newNode, function (loadedNode) {
                loadedNode = loadedNode || newNode
                loadedNode.isLoaded = true;
                // preserve the "dropped" flag
                loadedNode.dropped = newNode.dropped;
                _this.fireCallback(callback, "node", [loadedNode]);
            }, palette);
            return;
        }

        if (newNode.wizardConstructor) {
            this.logInfo("creating wizard with constructor: " + newNode.wizardConstructor);
            var wizard = isc.ClassFactory.newInstance(newNode.wizardConstructor,
                                                      newNode.wizardDefaults);
            // ask the wizard to go through whatever steps
            wizard.getResults(newNode, function (results) {
                // accept either a paletteNode or editNode (detect via liveObject)
                if (!results.liveObject) {
                    results = palette.makeEditNode(results);
                }
                _this.fireCallback(callback, "node", [results]);
            }, palette);
            return;
        }

        this.fireCallback(callback, "node", [newNode]);
    },

    // Gets the tree of editNodes being edited by this editContext. We create the tree
    // here, since the interface can't do it in initWidget.
    getEditNodeTree : function () {
        if (!this.editNodeTree) {
            // NOTE: there really is no reasonable default for rootComponent, since its type
            // determines what can be dropped.  This default will create a tree that won't accept
            // any drops, but won't JS error
            var rootComponent = this.rootComponent || { _constructor: "Object" },
                rootType = isc.isA.Class(rootComponent) ? rootComponent.Class :
                                                          rootComponent._constructor,
                rootLiveObject = this.rootLiveObject || rootComponent;

            var rootNode = {
                type: rootType,
                _constructor: rootType,
                defaults : rootComponent,
                liveObject: rootLiveObject
            };

            this.editNodeTree = isc.Tree.create({
                idField:"ID",
                root : rootNode,
                // HACK: so that all nodes can be targetted for D&D
                isFolder : function () { return true; }
            });
        }

        return this.editNodeTree;
    },

    getEditNodeArray : function () {
        return this.getEditNodeTree().getAllNodes();
    },

    //>!BackCompat 2011.06.25
    getEditComponents : function () {
        return this.getEditNodeArray();
    },
    //<!BackCompat

    // tests whether the targetNode can accept a newNode of type "type"
    canAddToParent : function (targetNode, type) {
        var liveObject = targetNode.liveObject;
        if (isc.isA.Class(liveObject)) {
            return (liveObject.getObjectField(type) != null);
        }
        // still required for MenuItems and ListGridFields, where the live object is not a Class
        return (isc.DS.getObjectField(targetNode, type) != null);
    },

    //> @method EditContext.removeAll()
    // Removes all +link{EditNode,EditNodes} from the EditContext, but does not destroy
    // the +link{EditNode.liveObject,liveObjects}.
    // @visibility external
    //<
    removeAll : function () {
        var data = this.getEditNodeTree();
        var rootChildren = data.getChildren(data.getRoot()).duplicate();
        for (var i = 0; i < rootChildren.length; i++) {
            this.removeNode(rootChildren[i]);
        }
    },

    //> @method EditContext.destroyAll()
    // Removes all +link{EditNode,EditNodes} from the EditContext, and calls
    // +link{Canvas.destroy(),destroy()} on the
    // +link{EditNode.liveObject,liveObjects}.
    // @visibility external
    //<
    destroyAll : function () {
        var data = this.getEditNodeTree();
        var rootChildren = data.getChildren(data.getRoot()).duplicate();
        for (var i = 0; i < rootChildren.length; i++) {
            this.destroyNode(rootChildren[i]);
        }
    },

    // remove an editNode from the tree
    removeNode : function (editNode, skipLiveRemoval) {
        var data = this.getEditNodeTree();


        // remove the corresponding component from the object model
        var parentNode = data.getParent(editNode);
        var liveChild = this.getLiveObject(editNode);
        var liveParent = this.getLiveObject(parentNode);

        // remove the node from the tree
        data.remove(editNode);

        if (skipLiveRemoval) return;

        if (liveParent && liveChild) {

            //this.logWarn("removing with defaults: " + this.echo(editNode.defaults));

            isc.DS.removeChildObject(liveParent, editNode.type, liveChild);
        }
    },

    //>!BackCompat 2011.06.25
    removeComponent : function (editNode, skipLiveRemoval) { // old name
        return this.removeNode(editNode, skipLiveRemoval);
    },
    //<!BackCompat

    // destroy an editNode in the tree, including it's liveObject
    destroyNode : function (editNode) {
        var liveObject = this.getLiveObject(editNode);
        this.removeNode(editNode);
        // if it has a destroy function, call it.  Otherwise we assume garbage collection will
        // work
        if (liveObject.destroy) liveObject.destroy();
    },

    //>!BackCompat 2011.06.25
    destroyComponent : function (editNode) { // old name
        return this.destroyNode(editNode);
    },
    //<!BackCompat

    // EditFields : optional lists of fields that can be edited in an EditContext
    // ---------------------------------------------------------------------------------------

    getEditDataSource : function (canvas) {
        return isc.DataSource.getDataSource(canvas.editDataSource || canvas.Class ||
                                            this.editDataSource);
    },

    // fields to edit:
    // - application-specific: two different editing applications may edit the same type of
    //   component (eg a ListViewer) exposing different sets of properties
    //   - the DataSource may not even represent the full set of properties, but regardless,
    //     can act as a default list of fields and reference properties for those fields
    // - on an application-specific basis, should be able to have a base set of fields, plus
    //   additions

    // get list of editable fields for a component.  May be a mix of string field names and
    // field objects
    _getEditFields : function (canvas) {
        // combine the baseEditFields and editFields properties
        var fields = [];
        fields.addList(canvas.baseEditFields);

        fields.addList(canvas.editFields);

        // HACK: set any explicitly specified fields to be visible, since many fields in the
        // current widget DataSources are set to visible=false to suppress them in editing
        // demos.  If a field is explicitly specified in editFields, we want it to be shown
        // unless they've set "visible" explicitly
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (field.visible == null) field.visible = true;
        }

        // if this is an empty list, take all the fields from the DataSource
        if (fields.length == 0) {
            fields = this.getEditDataSource(canvas).getFields();
            fields = isc.getValues(fields);
        }
        return fields;
    },

    // get the list of editable fields as an Array of Strings
    getEditFieldsList : function (canvas) {
        var fieldList = [],
            fields = this._getEditFields(canvas);
        // return just the name for any fields specified as objects
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (isc.isAn.Object(field)) {
                fieldList.add(field.name);
            } else {
                fieldList.add(field);
            }
        }
        return fieldList;
    },

    // get the edit fields, suitable for passing as "fields" to a dataBinding-aware component
    getEditFields : function (canvas) {
        var fields = this._getEditFields(canvas);
        // make any string fields into objects
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            if (isc.isA.String(field)) field = {name:field};
            // same hack as above to ensure visibility of explicitly specified fields, for
            // fields specified as just Strings
            if (field.visible == null) field.visible = true;
            fields[i] = field;
        }

        return fields;
    },

    // Serializing
    // --------------------------------------------------------------------------------------------
    // Take a tree of editNodes and produce a data structure that can be serialized to produce
    // actual XML or JSON source code



    //>!BackCompat 2013.09.27
    serializeComponents : function (serverless, includeRoot) {
        return this.serializeAllEditNodes(serverless, includeRoot);
    },
    //<!BackCompat

    //> @method editContext.serializeAllEditNodes()
    // Serialize the tree of +link{EditNode,EditNodes} to an XML representation
    // of +link{PaletteNode,PaletteNodes}. The result can be supplied to
    // +link{addPaletteNodesFromXML(),addPaletteNodesFromXML()} to recreate
    // the EditNodes.
    //
    // @param [serverless] (Boolean) If true, specify DataSources in full rather than
    //                               assuming they can be downloaded from the server.
    //                               Defaults to false.
    // @return (String) an XML representation of PaletteNodes which can be used to
    //                  recreate the tree of EditNodes.
    // @see addPaletteNodesFromXML
    // @visibility external
    //<
    serializeAllEditNodes : function (serverless, includeRoot) {
        // we flatten the Tree of objects into a flat list of top-level items
        // to serialize.  Nesting (eg grid within Layout) is accomplished by
        // having the Layout refer to the grid's ID.
        var data = this.getEditNodeTree();
        var nodes = includeRoot ? [data.root] : data.getChildren(data.root).duplicate();
        return this.serializeEditNodes(nodes, serverless);
    },

    //> @method editContext.serializeAllEditNodesAsJSON()
    // Encode the tree of +link{EditNode,EditNodes} to a JSON representation
    // of +link{PaletteNode,PaletteNodes}. The result can be supplied to
    // +link{addPaletteNodesFromJSON(),addPaletteNodesFromJSON()} to recreate
    // the EditNodes.
    //
    // @param [serverless] (Boolean) If true, specify DataSources in full rather than
    //                               assuming they can be downloaded from the server.
    //                               Defaults to false.
    // @return (String) a JSON representation of PaletteNodes which can be used to
    //                  recreate the tree of EditNodes.
    // @see addPaletteNodesFromJSON
    // @visibility external
    //<
    serializeAllEditNodesAsJSON : function (serverless, includeRoot) {
        // we flatten the Tree of objects into a flat list of top-level items
        // to serialize.  Nesting (eg grid within Layout) is accomplished by
        // having the Layout refer to the grid's ID.
        var data = this.getEditNodeTree();
        var nodes = includeRoot ? [data.root] : data.getChildren(data.root).duplicate();
        return this.serializeEditNodesAsJSON(nodes, serverless);
    },


    //> @method editContext.serializeEditNodes()
    // Serialize the provided +link{EditNode,EditNodes} to an XML
    // representation of +link{PaletteNode,PaletteNodes}. Note that the
    // EditNodes must have been added to this EditContext. The result can be
    // supplied to +link{addPaletteNodesFromXML(),addPaletteNodesFromXML()} to
    // recreate the EditNodes.
    //
    // @param nodes (Array of EditNode) EditNodes to be serialized
    // @param [serverless] (Boolean) If true, specify DataSources in full rather than
    //                               assuming they can be downloaded from the server.
    //                               Defaults to false.
    // @return (String) an XML representtion of the provided EditNodes
    // @visibility external
    //<
    // NOTE: the "nodes" passed to this function need to be part of the Tree that's available
    // as this.getEditNodeTree().  TODO: generalized this so that it takes a Tree, optional nodes, and
    // various mode flags like serverless.
    serializeEditNodes : function (nodes, serverless) {
        if (!nodes) return null;

        return this._serializeEditNodes(nodes, serverless);
    },

    //> @method editContext.serializeEditNodesAsJSON()
    // Serialize the provided +link{EditNode,EditNodes} to a JSON
    // representation of +link{PaletteNode,PaletteNodes}. Note that the
    // EditNodes must have been added to this EditContext. The result can be
    // supplied to +link{addPaletteNodesFromJSON(),addPaletteNodesFromJSON()} to
    // recreate the EditNodes.
    //
    // @param nodes (Array of EditNode) EditNodes to be serialized
    // @param [serverless] (Boolean) If true, specify DataSources in full rather than
    //                               assuming they can be downloaded from the server.
    //                               Defaults to false.
    // @return (String) a JSON representtion of the provided EditNodes
    // @visibility external
    //<
    serializeEditNodesAsJSON : function (nodes, serverless) {
        if (!nodes) return null;

        return this._serializeEditNodes(nodes, serverless, "json");
    },

    _serializeEditNodes : function (nodes, serverless, format) {
        if (!isc.isAn.Array(nodes)) nodes = [nodes];

        // add autoDraw to all non-hidden top-level components
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i] = isc.addProperties({}, nodes[i]),
                iscClass = isc.ClassFactory.getClass(node.type),
                defaults = node.defaults = isc.addProperties({}, node.defaults);

            //this.logWarn("considering node: " + this.echo(topNode) +
            //             " with defaults: " + this.echo(defaults));
            if (iscClass && iscClass.isA("Canvas") && defaults &&
                defaults.visibility != isc.Canvas.HIDDEN && defaults.autoDraw !== false)
            {
                defaults.autoDraw = true;
            }
        }

        // if serverless is set we will actually output DataSources in their entirety.
        // Otherwise, we'll just output a special tag that causes the DataSource to be loaded
        // as the server processes the XML format.
        this.serverless = serverless;

        this.defaultsBlocks = [];
        this.map("getSerializeableTree", nodes);

        this.serverless = null;

        var result = (format == "json" ? isc.JSON.encode(this.defaultsBlocks) : isc.EditContext.serializeDefaults(this.defaultsBlocks));

        return result;
    },

    // arrange the initialization data into a structure suitable for XML serialization.  This
    // will:
    // - grab just the defaults portion of each editNode (what we serialize)
    // - flatten hierarchies: all Canvas-derived components will be at top-level,
    //   members/children arrays will contain references to these children
    // - ensure DataSources are only listed once since multiple components may refer to the
    //   same DataSource
    getSerializeableTree : function (node, dontAddGlobally) {
        // Give the liveObject a chance to update the editNode
        var liveObject = node.liveObject;
        if (liveObject && liveObject.updateEditNode && liveObject.editContext && liveObject.editNode) {
            node.liveObject.updateEditNode(node.liveObject.editContext, node.liveObject.editNode);
        }

        var type = node.type,
            // copy defaults for possible modification
            defaults = isc.addProperties({}, node.defaults);
        // if this node is a DataSource (or subclass of DataSource)
        var classObj = isc.ClassFactory.getClass(type);

        this.logInfo("node: " + this.echoLeaf(node) + " with type: " + type);

        if (classObj && classObj.isA("DataSource")) {
            var isMockDataSource = (node.liveObject.getClassName() == "MockDataSource");
            // check for this same DataSource already being saved out
            if (this.defaultsBlocks) {
                var existingDS = this.defaultsBlocks.find("ID", defaults.ID) ||
                                 this.defaultsBlocks.find("loadID", defaults.ID);
                if (existingDS && existingDS.$schemaId == "DataSource") return;
            }

            if (!this.serverless && !isMockDataSource) {
                // when serializing a DataSource, just output the loadID tag so that the
                // server outputs the full definition during XML processing on JSP load
                defaults = {
                    _constructor: "DataSource",
                    $schemaId: "DataSource",
                    loadID: defaults.ID
                };
            } else {
                // if running serverless, we can't rely on the server to fetch the definition
                // as part of XML processing during JSP load, so we have to write out a full
                // definition.  This works only for DataSources that don't require the server
                // to fetch and update data.
                // NOTE: since all DataSources in Visual Builder are always saved to the
                // server, an alternative approach would be to load the DataSource and capture
                // its defaults, as we do when we edit an existing DataSource.  However we
                // would still depend on getSerializeableFields() being correct, as we also use
                // it to obtain clean data when we begin editing a dynamically created
                // DataSource obtained from XML Schema (eg SFDataSource)
                var liveDS = node.liveObject;
                defaults = liveDS.getSerializeableFields();
                defaults._constructor = liveDS.Class;
                defaults.$schemaId = "DataSource";
            }
        }

        // Actions
        // By default these will be defined as simple objects in JS, but for saving in XML
        // we need to enclose them in <Action>...</Action> tags
        // (ensures that any specified mappings are rendered out as an array)
        // Catch these cases and store as a StringMethod object rather than the raw action
        // object - this will serialize correctly.
        isc.EditContext.convertActions(node, defaults, classObj);

        var treeChildren = this.getEditNodeTree().getChildren(node);
        if (!treeChildren) {
            if (this.defaultsBlocks) this.defaultsBlocks.add(defaults); // add as a top-level node
            return;
        }

        this.serializeChildData(defaults, treeChildren);

        // if we're not supposed to be global, return out defaults
        if (dontAddGlobally) return defaults;
        // otherwise add this node's data globally (we list top-most parents last)
        if (this.defaultsBlocks) this.defaultsBlocks.add(defaults);
    },

    //>!BackCompat 2013.09.25
    addChildData : function (parentData, childNodes) {
        return this.serializeChildData(parentData, childNodes);
    },
    //<!BackCompat

    serializeChildData : function (parentData, childNodes) {
        var ds = isc.DS.get(parentData._constructor);
        for (var i = 0; i < childNodes.length; i++) {
            var child = childNodes[i],
                childType = child.defaults._constructor,
                // copy defaults for possible modification
                childData = isc.addProperties({}, child.defaults),
                parentFieldName = childData.parentProperty || ds.getObjectField(childType),
                parentField = ds.getField(parentFieldName);

            this.logInfo("serializing: child of type: " + childType +
                         " goes in parent field: " + parentFieldName,
                         "editing");

            // all Canvii output individually, and their parents just output the Canvas ID.
            // NOTE: don't do this for _generated components, which include TabSet tabs and
            // SectionStack sections.
            if ((isc.isA.Canvas(child.liveObject) && !child.liveObject._generated) ||
                isc.isA.DataSource(child.liveObject))
            {
                if (isc.isA.DataSource(child.liveObject) && parentFieldName == "dataSource") {
                    // Don't add the "ref:" if the parentFieldName is "dataSource", since
                    // the dataSource field always takes a String ID. (The "ref:" used
                    // to be stripped off later, so just don't add it).
                    childData = childData.ID;
                } else {
                    childData = "ref:" + childData.ID;
                }
                this.getSerializeableTree(child);
            } else {
                // otherwise, serialize this child without adding it globally
                childData = this.getSerializeableTree(child, true);
            }

            var existingValue = parentData[parentFieldName];
            if (parentField.multiple) {
                // force multiple fields to Arrays
                if (!existingValue) existingValue = parentData[parentFieldName] = [];
                existingValue.add(childData);
            } else {
                parentData[parentFieldName] = childData;
            }
        }
    },

    //>!BackCompat 2013.09.25
    serializeEditComponents : function () {
        return this.serializeLiveObjects();
    },
    //<!BackCompat

    // get serializable data as an Array of Objects for the editNodes in this context, via
    // getting properties from the liveObjects and stripping it down to editFields (fields that
    // are allowed to be edited in the context), or the DataSource fields if no editFields were
    // declared.

    serializeLiveObjects : function () {
        // get all the widgets being edited
        var widgets = this.getEditNodeArray(),
            output = [];

        if (!widgets) return [];

        for (var i = 0; i < widgets.length; i++) {
            var child = widgets[i].liveObject,
                // get all properties that don't have default value
                props = child.getUniqueProperties(),
                editFields = this.getEditFieldsList(child);

            // add in the Class, which will be needed to recreate the widget, but which could never
            // have non-default value
            props._constructor = child.Class;

            // limit the data to just the fields listed in the DataSource
            props = isc.applyMask(props, editFields);

            output.add(props);
        }
        return output;
    },

    //>!BackCompat 2013.09.25
    loadNodeTreeFromXML : function (xmlString) {
        this.addPaletteNodesFromXML(xmlString);
    },
    //<!BackCompat 2013.09.25

    //> @method editContext.addPaletteNodesFromXML()
    // Recreate +link{EditNode,EditNodes} from an XML representation of
    // +link{PaletteNode,PaletteNodes} (possibly created by calling
    // +link{serializeAllEditNodes()} or +link{serializeEditNodes()}.
    // <P>
    // By default, components that have +link{Canvas.ID,global IDs} will not
    // actually be allowed to take those global IDs - instead, only widgets that have one of the
    // global IDs passed as the <code>globals</code> parameter will actually receive their global
    // IDs.  To override this behavior, pass the special value +link{RPCManager.ALL_GLOBALS}
    // for the <code>globals</code> parameter.
    //
    // @param xmlString (String) XML string
    // @param [parentNode] (EditNode) parent node (defaults to the root)
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @see serializeAllEditNodes()
    // @see serializeEditNodes()
    // @visibility external
    //<
    addPaletteNodesFromXML : function (xmlString, parentNode, globals, callback) {
        var self = this;

        //isc.logWarn(isc.echo(xmlString));

        isc.DMI.callBuiltin({
            methodName: "xmlToJS",
            arguments: [xmlString],
            callback: function (rpcResponse) {
                self.addPaletteNodesFromJS(rpcResponse.data, parentNode, globals, callback);
            }
        });
    },

    //> @method editContext.addPaletteNodesFromJSON()
    // Recreate +link{EditNode,EditNodes} from a JSON representation of
    // +link{PaletteNode,PaletteNodes} (possibly created by calling
    // +link{serializeAllEditNodesAsJSON()} or +link{serializeEditNodesAsJSON()}.
    // <P>
    // By default, components that have +link{Canvas.ID,global IDs} will not
    // actually be allowed to take those global IDs - instead, only widgets that have one of the
    // global IDs passed as the <code>globals</code> parameter will actually receive their global
    // IDs.  To override this behavior, pass the special value +link{RPCManager.ALL_GLOBALS}
    // for the <code>globals</code> parameter.
    //
    // @param jsonString (String) JSON string
    // @param [parentNode] (EditNode) parent node (defaults to the root)
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @see serializeAllEditNodesAsJSON()
    // @see serializeEditNodesAsJSON()
    // @visibility external
    //<
    addPaletteNodesFromJSON : function (jsonString, parentNode, globals, callback) {
        if (globals == null) globals = [];
        else if (!isc.isAn.Array(globals)) globals = [globals];

        isc.captureDefaults = true;

        var jsClassDefs = isc.JSON.decode(jsonString);


        var keepAllGlobals = (globals.length == 1 && globals[0] == isc.RPC.ALL_GLOBALS);
        for (var i = 0; i < jsClassDefs.length; i++) {
            var def = jsClassDefs[i],
                className = def._constructor
            ;
            this._replaceRefs(def, globals);
            if (className) {
                delete def._constructor;
                if (def.ID && !keepAllGlobals && !globals.contains(def.ID)) def.ID = "_" + def.ID;
                var instance = isc.ClassFactory.newInstance(className, def);
            }
        }

        isc.captureDefaults = null;
        var capturedComponents = this.getCapturedComponents();

        // Remove IDs that represent globals that should not be kept
        this._removeIDs(capturedComponents, globals);

        this.addFromPaletteNodes(capturedComponents, parentNode);
        this.fireCallback(callback, ["paletteNodes"], [capturedComponents]);
    },

    // Replace values of type "ref:<ID>" with actual instance
    _replaceRefs : function (def, keepGlobals) {
        var keepAllGlobals = (keepGlobals.length == 1 && keepGlobals[0] == isc.RPC.ALL_GLOBALS);

        for (var key in def) {
            var value = def[key];
            if (isc.isAn.Array(value)) {
                for (var i = 0; i < value.length; i++) {
                    if (isc.isA.String(value[i]) && value[i].startsWith("ref:")) {
                        var ref = value[i].replace("ref:", "");
                        if (!keepAllGlobals && !keepGlobals.contains(ref)) ref = "_" + ref;
                        value[i] = window[ref];
                    } else if (isc.isAn.Object(value[i])) {
                        this._replaceRefs(value[i], keepGlobals);
                    }
                }
            } else if (isc.isAn.Object(value)) {
                this._replaceRefs(value, keepGlobals);
            }
        }
    },

    // Remove ID attributes whose value is not listed in keepGlobals
    _removeIDs : function (def, keepGlobals) {
        var keepAllGlobals = (keepGlobals.length == 1 && keepGlobals[0] == isc.RPC.ALL_GLOBALS);

        if (def.ID && !keepAllGlobals && !keepGlobals.contains(def.ID)) delete def.ID;

        for (var key in def) {
            if (key == "defaults") continue;
            var value = def[key];
            if (isc.isAn.Array(value)) {
                for (var i = 0; i < value.length; i++) {
                    if (isc.isAn.Object(value[i])) {
                        this._removeIDs(value[i], keepGlobals);
                    }
                }
            } else if (isc.isAn.Object(value)) {
                this._removeIDs(value, keepGlobals);
            }
        }
    },

    //> @method Callbacks.PaletteNodeCallback
    // Callback fired with the +link{PaletteNode,PaletteNodes} obtained asynchronously.
    // @param paletteNodes (Array of PaletteNode) an array of PaletteNodes
    // @visibility external
    //<

    //> @method editContext.getPaletteNodesFromXML()
    // Obtain +link{PaletteNode,PaletteNodes} from an XML representation,
    // but do not add them to the EditContext.
    //
    // @param xmlString (String) XML string
    // @param callback (PaletteNodeCallback) Callback used to return the PaletteNodes
    // @see Callbacks.PaletteNodeCallback()
    // @see serializeAllEditNodes()
    // @see serializeEditNodes()
    // @visibility external
    //<
    getPaletteNodesFromXML : function (xmlString, callback) {
        var self = this;

        //isc.logWarn(isc.echo(xmlString));

        isc.DMI.callBuiltin({
            methodName: "xmlToJS",
            arguments: [xmlString],
            callback: function (rpcResponse) {
                self.getPaletteNodesFromJS(rpcResponse.data, callback);
            }
        });
    },

    //>!BackCompat 2013.09.25
    loadNodeTreeFromJS : function (jsString) {
        return this.addPaletteNodesFromJS(jsString);
    },
    //<!BackCompat

    //> @method editContext.addPaletteNodesFromJS()
    // Add +link{PaletteNode,PaletteNodes} from a JavaScript source representation.
    // <P>
    // By default, components that have +link{Canvas.ID,global IDs} will not
    // actually be allowed to take those global IDs - instead, only widgets that have one of the
    // global IDs passed as the <code>globals</code> parameter will actually receive their global
    // IDs.  To override this behavior, pass the special value +link{RPCManager.ALL_GLOBALS}
    // for the <code>globals</code> parameter.
    //
    // @param jsCode (String) JavaScript code to eval.
    // @param [parentNode] (EditNode) parent node (defaults to the root)
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @visibility external
    //<
    addPaletteNodesFromJS : function (jsCode, parentNode, globals, callback) {
        if (globals == null) globals = [];
        else if (!isc.isAn.Array(globals)) globals = [globals];

        var self = this;
        this.getPaletteNodesFromJS(jsCode, function (paletteNodes) {
            // Remove IDs that represent globals that should not be kept
            this._removeIDs(paletteNodes, globals);

            self.addFromPaletteNodes(paletteNodes, parentNode);
            self.fireCallback(callback, ["paletteNodes"], [paletteNodes]);
        }, globals);
    },

    //> @method editContext.getPaletteNodesFromJS()
    // Obtain +link{PaletteNode,PaletteNodes} from a JavaScript source representation.
    // <P>
    // By default, components that have +link{Canvas.ID,global IDs} will not
    // actually be allowed to take those global IDs - instead, only widgets that have one of the
    // global IDs passed as the <code>globals</code> parameter will actually receive their global
    // IDs.  To override this behavior, pass the special value +link{RPCManager.ALL_GLOBALS}
    // for the <code>globals</code> parameter.
    //
    // @param jsCode (String) JavaScript code to eval.
    // @param callback (PaletteNodeCallback) Callback used to return the PaletteNodes
    // @param [globals] (Array of String) widgets to allow to take their global IDs
    // @see Callbacks.PaletteNodeCallback()
    // @visibility external
    //<
    getPaletteNodesFromJS : function (jsCode, callback, keepGlobals) {
        if (keepGlobals == null) keepGlobals = [];
        else if (!isc.isAn.Array(keepGlobals)) keepGlobals = [keepGlobals];

        var self = this;
        isc.captureDefaults = true;

        if (keepGlobals.length == 1 && keepGlobals[0] == isc.RPC.ALL_GLOBALS) {
            // suppress reportErrors
            isc.Class.globalEvalWithCapture(jsCode, function (globals, error) {
                // Note: this must happen first, before any other components are
                // created - otherwise we will trap them..
                isc.captureDefaults = null;
                var capturedComponents = self.getCapturedComponents(error);
                self.fireCallback(callback, ["paletteNodes"], [capturedComponents]);
            }, null, false);
        } else {
            // suppress reportErrors
            isc.Class.globalEvalAndRestore(jsCode, keepGlobals, function (globals, error) {
                // Note: this must happen first, before any other components are
                // created - otherwise we will trap them..
                isc.captureDefaults = null;
                var capturedComponents = self.getCapturedComponents(error);
                self.fireCallback(callback, ["paletteNodes"], [capturedComponents]);
            }, null, false);
        }

        isc.captureDefaults = null;
    },

    getCapturedComponents : function (error) {
        if (error) {
            isc.warn(
                "The following error occurred during loading of your view<br><br>: " + error +
                ".<br><br>  The portion of the view that loaded succesfully will be shown."
            );
        }

        var captured = isc.capturedComponents;
        isc.capturedComponents = null;

        var capturedIDs = (captured ? captured.getProperty("defaults").getProperty("ID") : null);
        this.logInfo("capturedComponents are: " + capturedIDs, "loadProject");

        return captured;
    },

    //>!BackCompat 2013.09.27
    addNodeTree : function (paletteNodes) {
        this.addFromPaletteNodes(paletteNodes);
    },
    //<!BackCompat

    //> @method EditContext.addFromPaletteNodes
    // Add the supplied +link{PaletteNode,PaletteNodes} to the parentNode, preserving internal
    // references from one supplied PaletteNode to another. This method should
    // be used with an array of possibly inter-related PaletteNodes (for
    // instance, those produced as a result of serialization via
    // +link{serializeAllEditNodes(),serializeAllEditNodes()}, rather than
    // calling +link{addFromPaletteNode(),addFromPaletteNode()} on each
    // individual PaletteNode.
    //
    // @param paletteNodes (Array of PaletteNode) array of PaletteNodes
    // @param [parentNode] (EditNode) parent to add to (defaults to the root)
    // @return (Array of EditNode) an array of the EditNodes added to the parentNode
    // @see addFromPaletteNode()
    // @visibility external
    //<
    addFromPaletteNodes : function (paletteNodes, parentNode) {
        //this.logWarn("paletteNodes: " + this.echoFull(paletteNodes), "loadProject");

        var data = this.getEditNodeTree();
        if (!parentNode) parentNode = data.getRoot();

        // When we evalWithCapture(), create() makes palette nodes instead of actual
        // instances.  This is a necessity so that initialization data can be captured cleanly.
        //
        // These palette nodes are arranged in a tree just like live components would be (eg
        // layout.members contains palette nodes for children).
        //
        // We need to traverse this tree and make a series of calls to
        // Palette.makeEditNode() and EditContext.addNode() to actual create live
        // components and editNodes from this captured data.

        this.componentsToCreate = [];
        this.addComponentCalls = [];
        this.requiredDataSources = [];

        // traverse all captured components (components that called create()), finding all
        // subcomponents that need to be represented as separate tree nodes (eg Tabs, which do
        // not directly call create, but should appear in the editTree).
        for (var i = 0; i < paletteNodes.length; i++) {
            this.findChildPaletteNodes(null, paletteNodes[i], null, paletteNodes);
        }

        // second traversal: paletteNodes is a flattened list of all components that would call
        // create(), and in the previous traversal we marked any component that was found in
        // the subtree of any other component as hasParent:true.  Any remaining paletteNodes
        // with no hasParent:true marker must be children of root
        for (var i = 0; i < paletteNodes.length; i++) {
            if (!paletteNodes[i].hasParent) {
                this.findChildPaletteNodes(parentNode, paletteNodes[i], null, paletteNodes);
            }
        }

        // preserve init order for the best chance of allowing application logic to function

        var pNode, parentPNode;
        // captured components (those that directly called create) are first, in order of
        // create() calls (which is leaf nodes first)
        for (var i = 0; i < paletteNodes.length; i++) {
            pNode = paletteNodes[i];
            pNode.component = this.makeEditNode(pNode);
        }

        // create all other components in tree traversal order
        for (var i = 0; i < this.componentsToCreate.length; i++) {
            pNode = this.componentsToCreate[i];
            if (!pNode.component) {
                pNode.component = this.makeEditNode(pNode);
            }
        }

        // lastly, link components into the project tree.  Because of the way we do our
        // traversal, these are not in an order that is ready for tree adds, that is, children
        // can appear before their parents because objects that directly call create() can
        // appear before the pseudo-objects (eg Sections) that they belong to (eg ListGrid can
        // appear before the Tab it should be added to).  However in order to eg, not reverse
        // Section Stack or FormItem order, we need to generally follow the order of traversal
        // that put together the addComponentCalls.
        // Approach: keep traversing the list trying to add children to parents until all nodes
        // have been added
        var oldLength = -1,
            calls = this.addComponentCalls,
            newCallOrder = []; // just for debugging

        // Set a flag to indicate to the special editModeSetDataSource() override that we are
        // loading a node tree from disk, and should fall back to the ordinary setDataSource()
        // method - otherwise, we'll end up with duplicates in the projectComponents tree
        // Also, disables markDirty while true.
        isc._loadingNodeTree = true;

        var nodesAddedToParentNode = [];

        while (calls.length > 0 && oldLength != calls.length) {
            oldLength = calls.length;
            var callsToTry = calls.duplicate();
            for (var i = 0; i < callsToTry.length; i++) {
                var call = callsToTry[i],
                    parentPNode = call[1],
                    pNode = call[0],
                    parentProperty = call[2];
                if (parentPNode.name == "/") {
                    var nodedAdded = this.addNode(pNode.component, parentNode);
                    nodesAddedToParentNode.add(nodeAdded);
                    calls.remove(call);
                    newCallOrder.add(call);
                } else if (data.contains(parentPNode.component)) {
                    var childComponent = pNode.component;
                    if (data.contains(childComponent)) {
                        // we've already added this child to the tree elsewhere.  This occurs
                        // for singletons like a DataSource which are shared between multiple
                        // components.  It's valid and intended in this case that the
                        // liveObject be shared, but we need a distinct Tree node, so make a
                        // copy
                        childComponent = isc.addProperties({}, childComponent);
                    }
                    var nodeAdded = this.addNode(childComponent, parentPNode.component, null, parentProperty);
                    if (parentPNode.component == parentNode) {
                        nodesAddedToParentNode.add(nodeAdded);
                    }
                    calls.remove(call);
                    newCallOrder.add(call);
                }
            }
        }

        delete isc._loadingNodeTree;

        // report the order of addComponent calls
        if (this.logIsDebugEnabled("loadProject")) {
            this.logDebug("addComponent() calls during project loading:", "loadProject");
            for (var i = 0; i < newCallOrder.length; i++) {
                var call = newCallOrder[i],
                    parentPNode = call[1],
                    pNode = call[0];
                this.logDebug(
                    "addComponent(" + this.echoLeaf(pNode) + "," + this.echoLeaf(parentPNode),
                    "loadProject"
                );
            }
        }

        if (calls.length > 0) {
            this.logWarn(
                "the following components could not be added to the project tree: " +
                isc.echoAll(calls.getProperty("0"))
            );
        }

        return nodesAddedToParentNode;
    },

    // create a paletteNode that will load the named DataSource dynamically
    makeDSPaletteNode : function (dsName, dsType) {
        var node = {
            ID: dsName,

            // for controlling drag and drop
            // XXX would be good to get actual type in case a component
            // declares that it can only bind to a specific DataSource, however,
            // "getDefinedDataSources" RPC does not currently return this.
            type: "DataSource",

            // for display in DataSources palette
            dsType: dsType || "DataSource",

            // for display in project tree
            title: dsName,
            icon: "DataSource.png",
            iconSize: 16,

            // set up deferred loading
            loadData: function (node, callback) {
                isc.DS.get(node.ID, function (ds) {
                    node.liveObject = ds;
                    // minimal information for serializing the DataSource.  See
                    // getSerializeableTree()
                    node.defaults = {
                        _constructor: "DataSource",
                        ID: ds.ID
                    };
                    node.isLoaded = true;
                    isc.Class.fireCallback(callback, "", [node]);
                });
            }
        };

        return node;
    },

    // recursively traverse a structure captured via evalWithCapture, modifying data so that it
    // is ready for addComponent().
    // - detect anywhere that a component is being initialized with data that should be
    //   represented as a separate component in the EditTree (eg a Layout member or TabSet tab)
    // - remove the subcomponent from the initialization data and create a separate
    //   paletteNode for it.  The cases are:
    //   - palette nodes captured by evalWithCapture, from components that called create()
    //   - tabs, sectionItems and other pseudo-objects that we represent in the editTree,
    //     detected because they are in a field whose type appears in the palette
    //   - for code that *was not* generated by Visual Builder, we may find eg a Layout member
    //     represented as an object with a _constructor property, as happens when you declare
    //     nested components in XML instead of breaking all components into independant
    //     top-level declarations.  Note these subcomponents will not be paletteNodes because
    //     create() was never called for them.  Instead, their format is similar to a TabSet
    //     tab or other pseudo-object
    // - generate and store the list of addComponent() calls needed to construct the tree.  We
    //   do these later in order to detect top-level components, and to maximally preserve
    //   initialization order.
    findChildPaletteNodes : function (parent, componentData, parentProperty, paletteNodes) {
        var componentType = componentData.type || componentData.className;

        var logEnabled = this.logIsInfoEnabled("loadProject"),
            logDebugEnabled = this.logIsDebugEnabled("loadProject");

        if (logEnabled) {
            this.logInfo(
                "inspecting defaults of component: " + this.echoLeaf(componentData) + " of type: " + componentType,
                "loadProject"
            );
        }

        var defaults = componentData.defaults,
            loader = this;

        // search for child components that should also be added to the project tree
        var childComponents = [],
            singleArray = [],
            componentDS = isc.DS.get(componentType);
        for (var propName in defaults) {
            var propValues = defaults[propName];

            if (!isc.isAn.Array(propValues)) {
                singleArray[0] = propValues;
                propValues = singleArray;
            } else if (logDebugEnabled) {
                this.logDebug(
                    "checking Array property: " + propName + ", value: " + this.echoLeaf(propValues) +
                    (fieldSchema ? " with schema: " + fieldSchema : ""),
                    "loadProject"
                );
            }

            var field = componentDS ? componentDS.getField(propName) : null,
                fieldType = field ? field.type : null,
                fieldSchema = isc.DS.get(fieldType),
                foundChildren = false;

            for (var i = 0; i < propValues.length; i++) {
                var propValue = propValues[i];
                if (logDebugEnabled) {
                    this.logDebug(
                        "checking property: " + propName + ", value: " + this.echoLeaf(propValue),
                        "loadProject"
                    );
                }

                // found a component captured by evalWithCapture (called create())
                if (paletteNodes.contains(propValue)) {
                    if (logEnabled) {
                        this.logInfo(
                            "found capturedComponent: " + this.echoLeaf(propValue) + " under property: " +
                            propName + " of component: " + this.echoLeaf(componentData),
                            "loadProject"
                        );
                    }
                    childComponents.add([propName, propValue]);
                    foundChildren = true;
                    continue;
                }

                if (propValue == null) {
                    this.logInfo("null property: " + propName + " on component: " + this.echoLeaf(componentData));
                }

                // detect pseudo-objects (eg tabs):
                // if the field is declared as complex type *and* items of this class can be
                // created from the palette (so clearly it is represented in the component tree).
                // Note that this means different editors may treat different objects as tree
                // nodes, for example, fields of a ListGrid.
                var childType = (propValue ? propValue._constructor : null) || fieldType,
                    childClass = isc.ClassFactory.getClass(childType);

                if (
                    fieldSchema && (
                        (childClass && childClass.isA(isc.Canvas)) ||
                        (childClass && childClass.isA(isc.DataSource)) ||
                        (childClass && childClass.isA(isc.FormItem)) ||
                        (this.findPaletteNode("type", childType)) ||
                        (this.findPaletteNode("className", childType))
                    )
                ) {
                    if (logEnabled) {
                        this.logInfo(
                            "found palettized component: " + this.echoLeaf(propValue) +
                            " of type: " + childType + " under property: " + propName +
                            " of component: " + this.echoLeaf(componentData),
                            "loadProject"
                        );
                    }

                    // A String in an Object slot should be the ID of a component that was
                    // already created.  NOTE: tab.pane can be a String that refers to a
                    // component that was created *after* the TabSet, however this code does
                    // handle that case since capturedComponents contains all components that
                    // called create()
                    if (isc.isA.String(propValue)) {
                        var refComponent = paletteNodes.find("ID", propValue);
                        if (refComponent == null) {
                            // detect fields of DataSource type with String values referring to
                            // DataSources that don't exist in the file.  This can happen with
                            // code not generated by Visual Builder.  If these DataSources are
                            // known (they appear in the dataSourceList loaded from the
                            // server), create a paletteNode that will load them automagically.
                            if (isc.DataSource.isA(fieldType)) {
                                var knownDS = this.findPaletteNode("ID", propValue);
                                if (true) {
                                    refComponent = this.makeDSPaletteNode(propValue);
                                }
                            }
                            if (refComponent == null) continue;
                        }
                        childComponents.add([propName, refComponent]);
                    } else {
                        var childDefaults = propValue;
                        childComponents.add([propName, {
                            ID : childDefaults.ID,
                            name : childDefaults.name,
                            type : childType,
                            defaults : childDefaults
                        }]);
                    }

                    foundChildren = true;
                }
            }

            if (foundChildren) delete defaults[propName];
        }

        // find the existing palette node for this class, if any, in order to pick up the icon
        // to use in the project tree
        var pNode = this.findPaletteNode("type", componentType) || this.findPaletteNode("className", componentType);

        if (pNode) {
            componentData.icon = componentData.icon || pNode.icon;
            componentData.iconSize = componentData.iconSize || pNode.iconSize;
            componentData.showDropIcon = componentData.showDropIcon || pNode.showDropIcon;
        }

        // collect all the components that should be created and the calls to addComponent()
        // that need to happen
        this.componentsToCreate.add(componentData);
        if (parent != null) {
            componentData.hasParent = true;
            this.addComponentCalls.add([componentData, parent, parentProperty]);
        }

        // recurse to handle the children of this component
        if (childComponents.length > 0) {
            for (var i = 0; i < childComponents.length; i++) {
                this.findChildPaletteNodes(componentData, childComponents[i][1], childComponents[i][0], paletteNodes);
            }
        }
    },

    // ---------------------------------------------------------------------------------------

    enableEditing : function (editNode) {
        var liveObject = editNode.liveObject;
        if (liveObject.setEditMode) {
            liveObject.setEditMode(true, this, editNode);
        } else {
            // We're trying enable editing on something that isn't a Canvas or a FormItem.
            // Assume that it needs no special logic beyond setting the editNode, editContext
            // and editingOn flag
            liveObject.editContext = this;
            liveObject.editNode = editNode;
            liveObject.editingOn = true;
        }
    },

    // Applying Properties to EditNodes
    // ---------------------------------------------------------------------------------------

    //> @method editContext.setNodeProperties()
    // Update an editNode's serializable "defaults" with the supplied properties. If you
    // wish to remove a property from the defaults (rather than setting it to null), then
    // use +link{removeNodeProperties(),removeNodeProperties()} instead.
    // @param editNode (EditNode) the editNode to update
    // @param properties (Canvas Properties) the properties to apply
    // @param [skipLiveObjectUpdate] (Boolean) whether to skip updating the
    //                                         +link{EditNode.liveObject,liveObject},
    //                                         e.g. if you have already updated the liveObject
    // @see removeNodeProperties
    // @visibility external
    //<

    setNodeProperties : function (editNode, properties, skipLiveObjectUpdate) {


        if (this.logIsDebugEnabled("editing")) {
            this.logDebug("with editNode: " + this.echoLeaf(editNode) +
                          " applying properties: " + this.echo(properties), "editing");
        }

        if (!editNode.defaults) editNode.defaults = {}

        // update the initialization / serializeable data
        isc.addProperties(editNode.defaults, properties);

        // update the component node with the new ID
        if (editNode.defaults.ID != null) editNode.ID = editNode.defaults.ID;

        // update the live object, unless we're skipping that
        var targetObject = editNode.liveObject;
        if (targetObject && !skipLiveObjectUpdate) {
            // Name property changes must force a remove/add of the node (such as name
            // on a FormItem). This is specified in the "rebuildOnChange" property of the
            // parent property.
            var theTree = this.getEditNodeTree(),
                parentComponent = this.getEditNodeTree().getParent(editNode),
                parentSchema = (parentComponent ? isc.DS.get(parentComponent.type) : null),
                parentLiveObject = (parentComponent ? parentComponent.liveObject : null),
                parentFieldName = (parentLiveObject ? isc.DS.getObjectField(parentLiveObject, editNode.type) : null),
                parentField = (parentFieldName ? parentSchema.fields[parentFieldName] : null)
            ;
            if (properties.name != null && parentField && parentField.rebuildOnChange && parentField.rebuildOnChange.toLowerCase() == "true") {
                var index = theTree.getChildren(parentComponent).findIndex(editNode);

                this.logInfo("using remove/re-add cycle to modify liveObject: " +
                            isc.echoLeaf(targetObject) + " within parent node " +
                            isc.echoLeaf(parentComponent));

                this.removeNode(editNode);

                // update the node with the new name and add it
                editNode.name = editNode.ID = properties.name;
                delete properties.name;

                this.addNode(editNode, parentComponent, index);

                // collect the newly created live object
                targetObject = this.getLiveObject(editNode);
            }

            // update the live object
            if (targetObject.setEditableProperties) {
                // instance of an SC class (or something else that implements a
                // setEditableProperties API)
                targetObject.setEditableProperties(properties);
                if (targetObject.markForRedraw) targetObject.markForRedraw();
                // NOTE: for FormItems, causes parent redraw
                else if (targetObject.redraw) targetObject.redraw();
            } else {
                // for objects that never become ISC classes (MenuItems, ListGrid fields),
                // call an overridable method on the parent if it exists
                var parentComponent = this.getEditNodeTree().getParent(editNode),
                    parentLiveObject = parentComponent ? parentComponent.liveObject : null;
                if (parentLiveObject && parentLiveObject.setChildEditableProperties)
                {
                    parentLiveObject.setChildEditableProperties(targetObject, properties,
                                                                editNode, this);
                } else {
                    // fall back to just applying the properties
                    isc.addProperties(targetObject, properties);
                }
            }

            this.markForRedraw();
        } // skipLiveObjectUpdate
    },

    //> @method editContext.removeNodeProperties()
    // Removes the specified properties from an editNode's serializable "defaults".
    // Note that the +link{EditNode.liveObject,liveObject} is <u>not</u> updated by this method.
    // To set a property to null (rather than removing it), use
    // +link{setNodeProperties(),setNodeProperties()} instead.
    // @param editNode (EditNode) the editNode to update
    // @param properties (Array of String) an array of property names to remove
    // @see setNodeProperties()
    // @visibility external
    //<
    removeNodeProperties : function (editNode, properties) {
        if (!isc.isAn.Array(properties)) properties = [properties];
        properties.map (function (property) {
            delete editNode[property];
        });
    },

    // ---------------------------------------------------------------------------------------

    // The "wrapperForm" is a DynamicForm that we auto-create as a container for a FormItem dropped
    // directly onto a Canvas, Layout or whatever.  We're using autoChild-like semantics here so
    // that you can provide your own settings for the generated form.  addWithWrapper() is also
    // used to wrap DrawItems in a DrawPane, and the third argument, wrapDrawPane, is a boolean
    // flag to distinguish the desired wrapper.
    wrapperFormDefaults: {
        _constructor: "DynamicForm"
    },
    wrapperDrawPaneDefaults: {
        _constructor: "DrawPane"
    },
    addWithWrapper : function (childNode, parentNode, wrapDrawPane) {
        var wrapForm = !wrapDrawPane,
            wrapperDefaults = (wrapDrawPane ? this.wrapperDrawPaneDefaults : this.wrapperFormDefaults),
            defaults = isc.addProperties({}, wrapperDefaults),
            paletteNode = {
                type: wrapperDefaults._constructor,
                defaults : defaults
            };

        // if this FormItem belongs to a DataSource, the wrapper form needs to use it too
        if (wrapForm && childNode.liveObject.schemaDataSource) {
            var item = childNode.liveObject;
            defaults.doNotUseDefaultBinding = true;
            defaults.dataSource = item.schemaDataSource;
            defaults.serviceNamespace = item.serviceNamespace;
            defaults.serviceName = item.serviceName;
        }
        var wrapperNode = this.makeEditNode(paletteNode);

        // add the wrapper to the parent
        this.addNode(wrapperNode, parentNode);
        // add the child node to the wrapper
        return this.addNode(childNode, wrapperNode);
    }
});

//> @groupDef devTools
// The Dashboards & Tools framework enables you to build interfaces in which a set of UI
// components can be edited by end users, saved and later restored.
// <P>
// This includes interfaces such as:
// <ul>
// <li> <b>Dashboards</b>: where a library of possible widgets can be created &
// configured then stored for future use and shared with other users
// <li> <b>Diagramming & Flowchart tools</b>: tools similar to Visio&trade; which allows users
// to use shapes and connectors to create a flowchart or diagram
// <li> <b>Form Builders &amp; Development Tools</b>: tools which enable end users to create
// new forms or entirely new screens and add them to the application on the fly
// </ul>
// <P>
// The basic building blocks of the Dashboards & Tools framework are:
// <ul>
// <li> <i>Palettes</i>: palettes create UI components on the fly from stored data.  Palettes
// come in a variety of flavors which implement different UI gestures for creating components
// (e.g. drag from tree, pick from menu, etc)
// <li> <i>EditContexts</i>: an edit context tracks a set of components that are being edited.
// Different EditContexts offer different built-in user interactions for editing the
// components they track, for example, a tree-based EditContext can provide an interface for
// managing relationships between components via drag and drop, and a Canvas-based EditContext
// can automatically store changes to position and size.
// </ul>
// <h3>Defaults vs LiveObject</h3>
// The Dashboards & Tools framework is careful to maintain a distinction between the current state
// of the live UI component and the data that should actually be persisted and used to
// re-create the component.  For example:
// <ul>
// <li> a portlet may be showing a border or brightened background color to hilite it within
// the tool, but this should not be saved as part of the portlet state
// <li> a component may have a current width of 510 pixels when viewed within a tool, but what
// should persist is the configured width of 40% of available space
// <li> in a development tool, a component such a Window automatically creates subcomponents
// such as a header - these should not persist because the Window knows how to create them
// without any additional data.  Only components specifically dragged into the Window by the
// end user should be persisted
// </ul>
// The data that will be saved and used to re-create the component is called the
// <b>defaults</b>.  A Palette, which creates a component from stored data, minimally requires
// the +link{SCClassName,type} of the component to create, and the defaults.  This
// information is captured by a +link{PaletteNode} (along with other options) - all types of
// Palettes work with a set or tree of paletteNodes.
// <P>
// When a component is created from a paletteNode, a "live object" is created from the
// defaults.  One PaletteNode may generate any number of live objects.  An +link{EditNode}
// combines the +link{editNode.liveObject,live object} along with the type and defaults
// needed to recreate the live object, and is used to track the created live object as it is
// further edited.  Essentially an EditNode is a copy of the PaletteNode in which a live object
// has been created and the editNode.defaults may now begin to change as the user edits the
// object they have created.
//
// <h3>Module requirements</h3>
// <b>NOTE:</b> you must load the Tools +link{group:loadingOptionalModules,Optional Module}
// for this framework.
// <P>
// Any tools that work with hierarchies of system components or derivations
// of them will also need the system schema which can be loaded by either of the
// following:
// <P>
// <i>JSP tag:</i> <pre>&lt;script&gt;&lt;isomorphic:loadSystemSchema /&gt;&lt;/script&gt;</pre>
// <P>
// <i>HTML tag:</i> <pre>&lt;SCRIPT SRC="../isomorphic/DataSourceLoader?dataSource=$systemSchema"&gt;&lt;/SCRIPT&gt;</pre>
//
// @title Dashboards & Tools Framework Overview
// @treeLocation Client Reference/Tools
// @visibility external
//<





//> @object PaletteNode
// An object representing a component which the user may create dynamically within an
// application.
// <P>
// A PaletteNode expresses visual properties for how the palette will display it (eg
// +link{paletteNode.title,title}, +link{paletteNode.icon,icon}) as well as instructions for
// creating the component the paletteNode represents (+link{paletteNode.type},
// +link{paletteNode.defaults}).
// <P>
// Various types of palettes (+link{ListPalette}, +link{TreePalette}, +link{MenuPalette},
// +link{TilePalette}) render a PaletteNode in different ways, and allow the user to trigger
// creation in different ways (eg drag and drop, or just click).  All share a common pattern
// for how components are created from palettes.
// <P>
// Note that in a TreePalette, a PaletteNode is essentially a +link{TreeNode} and can have
// properties expected for a TreeNode (eg,
// +link{TreeGrid.customIconDropProperty,showDropIcon}.  Likewise
// a PaletteNode in a MenuPalette can have the properties of a +link{MenuItem}, such as
// +link{MenuItem.enableIf}.
//
// @treeLocation Client Reference/Tools
// @visibility external
//<

//> @attr paletteNode.icon (SCImgURL : null : IR)
// Icon for this paletteNode.
//
// @visibility external
//<

//> @attr paletteNode.title (String : null : IR)
// Textual title for this paletteNode.
//
// @visibility external
//<

//> @attr paletteNode.type (SCClassName : null : IR)
// +link{SCClassName} this paletteNode creates, for example, "ListGrid".
//
// @visibility external
//<


//> @attr paletteNode.defaults (Properties : null : IR)
// Defaults for the component to be created from this palette.
// <P>
// For example, if +link{paletteNode.type} is "ListGrid", properties valid to pass to
// +link{Class.create,ListGrid.create()}.
//
// @visibility external
//<

//> @attr paletteNode.liveObject (Object : null : IR)
// For a paletteNode which should be a "singleton", that is, always provides the exact same
// object (==) rather than a dynamically created copy, provide the singleton object as
// <code>liveObject</code>.
// <P>
// Instead of dynamically creating an object from defaults, the <code>liveObject</code> will
// simply be assigned to +link{editNode.liveObject} for the created editNode.
//
// @visibility external
//<

//> @attr paletteNode.wizardConstructor (PaletteWizard : null : IR)
// A paletteNode that requires user input before component creation can occur
// may provide a <code>wizardConstructor</code> and +link{wizardDefaults} for the creation of
// a "wizard" component.
// <P>
// Immediately after creation, the wizard will then have the +link{paletteWizard.getResults()}
// method called on it, dynamically produced defaults.
//
// @visibility internal
//<

//> @attr paletteNode.wizardDefaults (PaletteWizard Properties : null : IR)
// Defaults for the wizard created to gather user input before a component is created from
// this PaletteNode.  See +link{wizardConstructor}.
//
// @visibility internal
//<



// PaletteWizard
// ---------------------------------------------------------------------------------------

//> @interface PaletteWizard
// Interface to be fulfilled by a "wizard" specified on a +link{PaletteNode} via
// +link{paletteNode.wizardConstructor}.
// @visibility internal
//<

//> @method paletteWizard.getResults()
// Single function invoked on paletteWizard.  Expects defaults to be asynchronously returned,
// after user input is complete, by calling the +link{Callback} provided as a parameter.
//
// @param callback (Callback) callback to be fired once this wizard completes.  Expects a
//                            single argument: the defaults
// @param paletteNode (PaletteNode) the paletteNode that specified this wizard
// @param palette (Palette) palette where creation is taking place
//
// @visibility internal
//<

//> @interface Palette
// An interface that provides the ability to create components from a +link{PaletteNode}.
//
// @treeLocation Client Reference/Tools
// @group devTools
// @visibility external
//<

isc.ClassFactory.defineInterface("Palette");

isc.Palette.addInterfaceProperties({
    //> @attr palette.defaultEditContext (EditContext : null : IRW)
    // Default EditContext that this palette should use.  Palettes generally create components via
    // drag and drop, but may also support creation via double-click or other UI idioms when a
    // defaultEditContext is set.
    // @visibility external
    //<

    //> @method palette.setDefaultEditContext()
    // Sets the default EditContext that this palette should use.  Palettes generally create components via
    // drag and drop, but may also support creation via double-click or other UI idioms when a
    // defaultEditContext is set.
    // @param defaultEditContext (EditContext) the default EditContext used by this Palette
    // @visibility external
    //<
    setDefaultEditContext : function (defaultEditContext) {
        this.defaultEditContext = defaultEditContext;

        // If the defaultEditContext does not have a defaultPalette, then set it
        if (defaultEditContext && !defaultEditContext.defaultPalette) {
            defaultEditContext.defaultPalette = this;
        }
    },

    //> @method palette.makeEditNode()
    // Given a +link{PaletteNode}, make an +link{EditNode} from it by creating a
    // +link{editNode.liveObject,liveObject} from the +link{paletteNode.defaults}
    // and copying presentation properties (eg +link{paletteNode.title,title}
    // to the editNode. If <code>editNodeProperties</code> is specified as an object on
    // on the paletteNode, each property in this object will also be copied across to
    // the editNode.
    // @param paletteNode (PaletteNode) paletteNode to create from
    // @return (EditNode) created EditNode
    //
    // @visibility external
    //<
    makeEditNode : function (paletteNode) {
        if (!paletteNode) paletteNode = this.getDragData();
        if (isc.isAn.Array(paletteNode)) paletteNode = paletteNode[0];

        var type = paletteNode.type || paletteNode.className;
        if (!isc.SGWTFactory.getFactory(type) && type.contains(".")) type = type.split(/\./).pop();

        var componentNode = {
            type : type,
            _constructor : type, // this is here just to match the defaults
            // for display in the target Tree
            title : paletteNode.title,
            icon : paletteNode.icon,
            iconSize : paletteNode.iconSize,
            showDropIcon : paletteNode.showDropIcon,
            useEditMask : paletteNode.useEditMask,
            autoGen : paletteNode.autoGen
        };

        // support arbitrary properties on the generated edit node
        // This allows 'loadData' to get at properties that might not otherwise be copied
        // across to the editNode from the paletteNode
        if (isc.isAn.Object(paletteNode.editNodeProperties)) {
            for (var prop in paletteNode.editNodeProperties) {
                componentNode[prop] = paletteNode.editNodeProperties[prop];
            }
        }

        // allow a maker function on the source data (synchronous)
        if (paletteNode.makeComponent) {
            componentNode.liveObject = paletteNode.makeComponent(componentNode);
            return componentNode;
        }

        // NOTE: IDs
        // - singletons may have an ID on the palette node.
        // - an ID may appear in defaults because palette-based construction is used to reload
        //   views, and in this case the palette node will be used once ever
        var defaults = paletteNode.defaults;
        componentNode.ID = paletteNode.ID ||
                (defaults ? isc.DS.getAutoId(defaults) : null);

        var clobberDefaults = true;

        if (paletteNode.loadData) {
            // deferred load node.  No creation happens for now; whoever receives this node is
            // expected to call the loadData function
            componentNode.loadData = paletteNode.loadData;
        } else if (paletteNode.wizardConstructor) {
            // wizard-based deferred construction
            componentNode.wizardConstructor = paletteNode.wizardConstructor;
            componentNode.wizardDefaults = paletteNode.wizardDefaults;
        } else if (paletteNode.liveObject) {
            // singleton, or already created component.  This means that rather than a new
            // object being instantiated each time, the same "liveObject" should be reused,
            // because multiple components will be accessing a shared object.
            var liveObject = paletteNode.liveObject;
            // handle global IDs
            if (isc.isA.String(liveObject)) liveObject = window[liveObject];
            componentNode.liveObject = liveObject
        } else {
            // create a live object from defaults
            componentNode = this.createLiveObject(paletteNode, componentNode);
            clobberDefaults = false;
        }

        // also pass the defaults. Note that this was overwriting a more detailed set of defaults
        // derived by the createLiveObject method; hence the introduction  of the condition
        if (clobberDefaults) {
            componentNode.defaults = isc.addProperties({}, paletteNode.defaults);
            delete componentNode.defaults[isc.gwtRef];
            delete componentNode.defaults[isc.gwtModule];
            delete componentNode.defaults["xsi:type"];
        }

        return componentNode;
    },

    //>!BackCompat 2013.09.20
    makeNewComponent : function (sourceData) {
       return this.makeEditNode(sourceData);
    },
    //<!BackCompat

    //> @attr palette.generateNames   (boolean : true : IR)
    // Whether created components should have their "ID" or "name" property automatically set
    // to a unique value based on the component's type, eg, "ListGrid0".
    //
    // @group devTools
    // @visibility external
    //<
    generateNames : true,

    typeCount : {},
    // get an id for the object we're creating, by type
    getNextAutoId : function (type) {
        if (type == null) {
            type = "Object";
        } else {
            // Use short IDs for objects created via SGWT reflection
            if (type.contains(".")) type = type.split(/\./).pop();
        }
        var autoId;
        this.typeCount[type] = this.typeCount[type] || 0;
        while (window[(autoId = type + this.typeCount[type]++)] != null) {}
        return autoId;
    },

    findPaletteNode : function (fieldName, value) {
        return null;
    },

    createLiveObject : function (paletteNode, editNode) {

        // put together an initialization data block
        var type = paletteNode.type || paletteNode.className;
        if (type.contains(".") && !isc.SGWTFactory.getFactory(type)) type = type.split(/\./).pop();

        var classObject = isc.ClassFactory.getClass(type),
            schema = isc.DS.getNearestSchema(type),
            defaults = {},
            // assume we should create standalone if there's no schema (won't happen anyway if
            // there's no class)
            createStandalone = (schema ? schema.shouldCreateStandalone() : true),
            paletteNodeDefaults = paletteNode.defaults || {};

        // suppress drawing for widgets
        if (classObject && classObject.isA("Canvas")) defaults.autoDraw = false;

        // If a title was explicitly passed in the sourceData, use it
        if (paletteNodeDefaults.title) {
            defaults.title = paletteNodeDefaults.title;
        }

        if (this.generateNames) {
            // generate an id if one wasn't specified
            var ID = editNode.ID || paletteNodeDefaults[schema.getAutoIdField()];
            if (ID == null) {
                ID = this.getNextAutoId(type);

                if (isc.isA.Class(classObject)) {
                    defaults.hasStableID = function () { return false; };
                }
            }
            editNode.ID = ID;

            // give the object an autoId in defaults
            defaults[schema.getAutoIdField()] = ID;

            // don't supply a title for contexts where the ID or name will automatically be
            // used as a title (currently just formItems), otherwise, it will be necessary to
            // change both ID/name and title to get rid of the auto-gen'd id
            if (
                schema &&
                schema.getField("title") &&
                !isc.isA.FormItem(classObject) &&
                !defaults.title
            ) {
                defaults.title = ID;
            }
        }

        defaults = editNode.defaults = isc.addProperties(
            defaults,
            this.componentDefaults,
            paletteNodeDefaults
        );
        delete defaults[isc.gwtRef];
        delete defaults[isc.gwtModule];
        // An xsi:type property in defaults should be dropped to avoid serializing because
        // it won't be valid without proper includes.
        delete defaults["xsi:type"];
        defaults._constructor = type;


        var classObject = isc.ClassFactory.getClass(type);
        for (var prop in defaults) {
            var val = defaults[prop];
            if (
                isc.isAn.Array(val) &&
                (!classObject || classObject.getInstanceProperty(prop) !== val)
            ) {
                val = defaults[prop] = val.duplicate();

                // Check for arrays of arrays.
                for (var i = val.length; i--; ) {
                    if (isc.isAn.Array(val[i])) {
                        val[i] = val[i].duplicate();
                    }
                }
            }
        }

        // create the live object from the init data
        // NOTE: components generated from config by parents (tabs, sectionStack sections,
        // formItems).  These objects:
        // - are created as an ISC Class by adding to a parent, and not before
        //   - in makeEditNode, don't create if there is no class or if the schema sets
        //     createStandalone=false
        // - destroyed by removal from the parent, then re-created by a re-add
        //   - re-add handled by addComponent by checking for destruction
        // - serialized as sub-objects rather than independent components
        //   - handled by checking for _generated during serialization
        //   - should be a default consequence of not having a class or setting
        //     createStandalone=false
        // The various checks mentioned above are redundant and should be unified and make able
        // to be declared in component schema

        // if there's no class for the item, or schema.createStandalone has been set false,
        // don't auto-create the component - assume the future parent of the component will
        // create it from data.  The explicit flag (createStandalone:false) is needed for
        // FormItems.  In particular, canvasItems require item.containerWidget to be defined
        // during init.
        var liveObject;
        if (classObject && createStandalone) {
            liveObject = isc.ClassFactory.newInstance(defaults);
        } else {
            // for the live object, just create a copy (NOTE: necessary because widgets
            // generally assume that it is okay to add properties to pseudo-objects provided as
            // init data)
            editNode.generatedType = true;
            liveObject = isc.shallowClone(defaults);
        }

        // store the new live object
        editNode.liveObject = liveObject;
        this.logInfo("palette created component, type: " + type +
                     ", ID: " + ID +
                     (this.logIsDebugEnabled("editing") ?
                         ", defaults: " + this.echo(defaults) : "") +
                     ", liveObject: " + this.echoLeaf(liveObject), "editing");
        return editNode;
    }
});

//> @class HiddenPalette
// A Palette with no visible representation that handles programmatic creation of components.
//
// @implements Palette
// @group devTools
// @treeLocation Client Reference/Tools/Palette
// @visibility external
//<
isc.defineClass("HiddenPalette", "Class", "Palette");

// ---------------------------------------------------------------------------------------

//> @class TreePalette
// A TreeGrid that implements the Palette behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
// <P>
// Each +link{TreeNode} within +link{treeGrid.data} can be a +link{PaletteNode}.
//
// @implements Palette
// @group devTools
// @treeLocation Client Reference/Tools/Palette
// @visibility external
//<

// Class will not work without TreeGrid
if (isc.TreeGrid) {

isc.defineClass("TreePalette", "TreeGrid", "Palette");

isc.TreePalette.addMethods({
    //> @attr treePalette.componentDefaults    (Object : null : IR)
    // Defaults to apply to all components originating from this palette.
    // @group devTools
    // @visibility external
    //<


    canDragRecordsOut:true,
    // add to defaultEditContext (if any) on double click
    recordDoubleClick : function () {
        var target = this.defaultEditContext;
        if (target) {
            if (isc.isA.String(target) && this.creator) target = this.creator[target];
            if (isc.isAn.EditContext(target)) {
                var node = this.makeEditNode(this.getDragData());
                if (node) {
                    if (target.getDefaultParent(node, true) == null) {
                        isc.warn("No default parent can accept a component of this type");
                    } else {
                        target.addNode(node);
                        isc.EditContext.selectCanvasOrFormItem(node.liveObject, true);
                    }
                }
            }
        }
    },

    findPaletteNode : function (fieldName, value) {
        return this.data ? this.data.find(fieldName, value) : null;
    },

    // NOTE: we can't factor this up to the Palette interface because it wouldn't override the
    // built-in implementation of transferDragData.
    transferDragData : function (targetFolder) {
        return [this.makeEditNode(this.getDragData())];
    }
});

}

// --------------------------------------------------------------------------------------------
//> @class ListPalette
// A ListGrid that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
// <P>
// Each +link{ListGridRecord} can be a +link{PaletteNode}.
//
// @implements Palette
// @group devTools
// @treeLocation Client Reference/Tools/Palette
// @visibility external
//<

// Class will not work without ListGrid
if (isc.ListGrid) {

isc.defineClass("ListPalette", "ListGrid", "Palette");

isc.ListPalette.addMethods({
    canDragRecordsOut:true,
    defaultFields : [ { name:"title", title:"Title" } ],

    // add to defaultEditContext (if any) on double click
    recordDoubleClick : function () {
        // NOTE: dup'd in TreePalette
        var target = this.defaultEditContext;
        if (target) {
            if (isc.isA.String(target)) target = isc.Canvas.getById(target);
            if (isc.isAn.EditContext(target)) {
                target.addNode(this.makeEditNode(this.getDragData()));
            }
        }
    },

    findPaletteNode : function (fieldName, value) {
        return this.data ? this.data.find(fieldName, value) : null;
    },

    // NOTE: we can't factor this up to the Palette interface because it wouldn't override the
    // built-in implementation of transferDragData.
    transferDragData : function () {
        return [this.makeEditNode(this.getDragData())];
    }
});

}

// --------------------------------------------------------------------------------------------
//> @class TilePalette
// A +link{TileGrid} that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
// <P>
// Each +link{TileGrid.tile} can be a +link{PaletteNode}.
//
// @implements Palette
// @group devTools
// @treeLocation Client Reference/Tools/Palette
// @visibility external
//<

// Class will not work without TileGrid
if (isc.TileGrid) {

isc.defineClass("TilePalette", "TileGrid", "Palette");

isc.TilePalette.addMethods({
    canDragRecordsOut: true,
    defaultFields: [
        {name: "title", title: "Title"}
    ],

    // add to defaultEditContext (if any) on double click
    recordDoubleClick : function () {
        var target = this.defaultEditContext;
        if (target) {
            if (isc.isA.String(target)) target = isc.Canvas.getById(target);
            if (isc.isAn.EditContext(target)) {
                target.addNode(this.makeEditNode(this.getDragData()));
            }
        }
    },

    findPaletteNode : function (fieldName, value) {
        return this.data ? this.data.find(fieldName, value) : null;
    },

    // NOTE: we can't factor this up to the Palette interface because it wouldn't override the
    // built-in implementation of transferDragData.
    transferDragData : function () {
        return [this.makeEditNode(this.getDragData())];
    }
});

}

// --------------------------------------------------------------------------------------------
//> @class MenuPalette
// A Menu that implements the +link{Palette} behavior, so it can be used as the source for
// drag and drop instantiation of components when combined with an +link{EditContext} as
// the drop target.
// <P>
// Each +link{MenuItem} can be a +link{PaletteNode}.
//
// @implements Palette
// @group devTools
// @treeLocation Client Reference/Tools/Palette
// @visibility external
//<

// Class will not work without Menu
if (isc.Menu) {

isc.defineClass("MenuPalette", "Menu", "Palette");

isc.MenuPalette.addMethods({
    canDragRecordsOut: true,

    // needed because the selection is what's dragged, and menus do not normally track a
    // selection
    selectionType: "single",

    // add to defaultEditContext (if any) on click
    itemClick : function (item) {
        var target = this.defaultEditContext;
        if (target) {
            if (isc.isA.String(target)) target = isc.Canvas.getById(target);
            if (isc.isAn.EditContext(target)) {
                target.addNode(this.makeEditNode(this.getDragData()));
            }
        }
    },

    findPaletteNode : function (fieldName, value) {
        return this.data ? this.data.find(fieldName, value) : null;
    },

    // NOTE: we can't factor this up to the Palette interface because it wouldn't override the
    // built-in implementation of transferDragData.
    transferDragData : function () {
        return [this.makeEditNode(this.getDragData())];
    }
});

}




// ---------------------------------------------------------------------------------------

//> @class EditPane
// A container that allows drag and drop instantiation of visual components from a
// +link{Palette}, and direct manipulation of the position and size of those components.
// <P>
// Any drag onto an EditPane from a Palette will add an EditNode created from the dragged
// PaletteNode.
//
// @group devTools
// @implements EditContext
// @treeLocation Client Reference/Tools/EditContext
// @visibility external
//<

// Schema definition for the EditPane class, in case we have not loaded the system schema.

if (!isc.DataSource.get("EditPane")) {
    isc.DataSource.create({
        ID: "EditPane",
        Contructor: "EditPane",
        addGlobalId:false,
        fields: [
            {name: "children", type: "Canvas", multiple: true}
        ]
    });
}

isc.ClassFactory.defineClass("EditPane", "Canvas", "EditContext");

isc.EditPane.addProperties({
    canAcceptDrop:true,
    contextMenu : {
        autoDraw:false,
        data : [{title:"Clear", click: "target.removeAll()"}]
    },

    editingOn:true,

    // drag selection properties
    canDrag:true,
    dragAppearance:"none",
    overflow:"hidden",
    selectedComponents: [],

    initWidget : function () {
        // We'll be the live object for the root node ... this gets picked up
        // later in getEditNodeTree
        this.rootLiveObject = this;
        this.rootComponent = {
            _constructor: "EditPane"
        };
        this.Super("initWidget", arguments);
    },

    // Component creation
    // ---------------------------------------------------------------------------------------

    // This is needed if the system schema has not been loaded
    getObjectField : function (type) {
        var classObject = isc.ClassFactory.getClass(type);
        if (isc.isA.Canvas(classObject)) {
            return "children";
        } else {
            return null;
        }
    },

    // on drop from a palette, add a new component
    drop : function () {
        var source = isc.EH.dragTarget;

        // if the source isn't a Palette, do standard drop interaction
        if (!source.isA("Palette")) return this.Super("drop", arguments);

        var data = source.transferDragData(),
        editNode = (isc.isAn.Array(data) ? data[0] : data);
        if (!editNode) return false;

        var editContext = this;
        this.requestLiveObject(editNode, function (editNode) {
            if (editNode) editContext.addNodeAtCursor(editNode);
        }, source)

        return isc.EH.STOP_BUBBLING;
    },

    nodeAdded : function (newNode) {

        // Flip it into edit mode depending on the setting on the VB instance
        if (this.creator && this.creator.editingOn) this.enableEditing(newNode);

        // Add an event mask if so configured
        if (newNode.useEditMask) newNode.liveObject.showEditMask();
    },

    // add a new component at the current mouse position
    addNodeAtCursor : function (component) {
        this.addNode(component);
        var liveObject = component.liveObject;
        liveObject.moveTo(this.getOffsetX(), this.getOffsetY());
    },

    // The EditPane itself is the default parent for added nodes
    getDefaultParent : function (newNode, returnNullIfNoSuitableParent) {
        return this.getEditNodeTree().getRoot();
    },

    // Component removal / destruction
    // ---------------------------------------------------------------------------------------

    // if a child is removed that is being edited, remove it from the list of edit components
    removeChild : function (child, name) {
        this.Super("removeChild", arguments);
        var node = this.getEditNodeArray().find("liveObject", child);
        if (node) {
            this.removeNode(node, true); // skip live removal, since that's been done
        }
        this.selectedComponents.remove(child);
    },

    removeSelection : function (target) {
        if (this.selectedComponents.length > 0) {
            while (this.selectedComponents.length > 0) {
                this.selectedComponents[0].destroy();
            }
        } else {
            target.destroy();
        }
    },

    // Thumbs, drag move and resize
    // ---------------------------------------------------------------------------------------

    click : function () {
        isc.Canvas.hideResizeThumbs();
    },

    // enable editing mode for the entire EditPane: turn editing on for all edit components
    setEditMode : function (editingOn) {
        if (editingOn == null) editingOn = true;
        if (this.editingOn == editingOn) return;
        this.editingOn = editingOn;

        var liveObjects = this.getEditNodeArray().getProperty("liveObject");
        liveObjects.map("setEditMode", editingOn, this);
    },

    // save new coordinates to defaults on resize or move
    childResized : function (liveObject) {
        var result = this.Super("childResized", arguments);

        this.saveCoordinates(liveObject);

        return result;
    },

    childMoved : function (liveObject, deltaX, deltaY) {
        var result = this.Super("childMoved", arguments);

        this.saveCoordinates(liveObject);

        if (!this._movingSelection) {
            // if this component is part of a selection, move the rest of the selected
            // components by the same amount
            var selection = this.selectedComponents;
            if (selection.length > 0 && selection.contains(liveObject)) {
                this._movingSelection = true;
                for (var i = 0; i < selection.length; i++) {
                    if (selection[i] != liveObject) {
                        selection[i].moveBy(deltaX, deltaY);
                    }
                }
                this._movingSelection = false;
            }
        }

        return result;
    },

    saveCoordinates : function (liveObject) {
        if (!this.persistCoordinates) return;

        //this.logWarn("saveCoordinates for: " + liveObject +
        //             ", editComponents are: " + this.echoAll(this.getEditNodeArray()));
        var component = this.getEditNodeArray().find("liveObject", liveObject);

        // can happen if we get a resized or moved notification while a component is being
        // added or removed
        if (!component) return;

        this.setNodeProperties(component, {
            left: liveObject.getLeft(),
            top: liveObject.getTop(),
            // Use percentage width or "*" if supplied
            width: liveObject._percent_width || liveObject._userWidth || liveObject.getWidth(),
            height: liveObject._percent_height || liveObject._userHeight || liveObject.getHeight()
        }, true);
    },

    // Serialization
    // ---------------------------------------------------------------------------------------

    //> @method editPane.getSaveData()
    // Returns an Array of +link{PaletteNode}s representing all current +link{EditNode}s in this
    // pane, suitable for saving and restoring via passing each paletteNode to +link{EditContext.addNode(),addNode()}.
    // @return (Array of PaletteNode) paletteNodes suitable for saving for subsequent restoration
    //
    // @visibility external
    //<
    getSaveData : function () {
        // get all the components being edited
        var data = this.getEditNodeTree(),
            editComponents = data.getChildren(data.getRoot()),
            allSaveData = [];
        for (var i = 0; i < editComponents.length; i++) {
            var component = editComponents[i],
                liveObject = component.liveObject;
            // save off just types and initialization data, not the live objects themselves
            var saveData = {
                type : component.type,
                defaults : component.defaults
            };
            // let the object customize it
            if (liveObject.getSaveData) saveData = liveObject.getSaveData(saveData);
            allSaveData.add(saveData);
        }
        return allSaveData;
    },

    // Hoop selection
    // --------------------------------------------------------------------------------------------

    // create selector hoop
    canMultiSelect:true,
    mouseDown : function () {
        if (!this.editingOn || !this.canMultiSelect ||
            // don't start hoop selection unless the mouse went down on the EditPane itself, as
            // opposed to on one of the live objects
            isc.EH.getTarget() != this) return;

        var target = isc.EH.getTarget();
        if (this.selector == null) {
            this.selector = isc.Canvas.create({
                autoDraw:false,
                keepInParentRect: true,
                left: isc.EH.getX(),
                top: isc.EH.getY(),
                redrawOnResize:false,
                overflow: "hidden",
                border: "1px solid blue"
            });
            this.addChild(this.selector);
        }
        this.startX = this.getOffsetX();
        this.startY = this.getOffsetY();

        this.resizeSelector();
        this.selector.show();
    },

    // resize hoop on dragMove
    dragMove : function() {
        if (this.selector) this.resizeSelector();
    },

    // hide selector hoop on mouseUp or dragStop
    mouseUp : function () {
        if (this.selector) this.selector.hide();
    },
    dragStop : function() {
        if (this.selector) this.selector.hide();
    },

    outlineBorderStyle : "2px dashed red",
    // add an outline, indicating selection to a set of components
    setOutline : function (components) {
        if (!components) return;
        if (!isc.isAn.Array(components)) components = [components];
        for (var i = 0; i < components.length; i++) {
            components[i]._editMask.setBorder(this.outlineBorderStyle);
        }
    },

    // clear outline on a set of components
    clearOutline : function (components) {
        if (!components) return;
        if (!isc.isAn.Array(components)) components = [components];
        for (var i = 0; i < components.length; i++) {
            components[i]._editMask.setBorder("none");
        }
    },

    // figure out which components intersect the selector hoop, and show the selected outline on
    // those
    updateCurrentSelection : function () {
        if (!this.children) return;
        var oldSelection = this.selectedComponents;

        // make a list of all the children which currently intersect the selection hoop
        this.selectedComponents = [];
        for (var i = 0; i < this.children.length; i++) {
            var child = this.children[i];
            if (this.selector.intersects(child)) {
                child = this.deriveSelectedComponent(child);
                if (child && !this.selectedComponents.contains(child)) {
                    this.selectedComponents.add(child);
                }
            }
        }

        // set outline on components currently within the hoop
        this.setOutline(this.selectedComponents);

        // de-select anything that is no longer within the hoop
        oldSelection.removeList(this.selectedComponents);
        this.clearOutline(oldSelection);

        // show selection in window.status

        var selection = this.selectedComponents.getProperty("ID");
        window.status = selection.length ? "Current Selection: " + selection : "";
    },

    // given a child in the editPane, derive the editComponent if there is one
    deriveSelectedComponent : function (comp) {
        // if the component has a master, it's either an editMask or a peer of some editComponent
        if (comp.masterElement) return this.deriveSelectedComponent(comp.masterElement);
        if (!comp.parentElement || comp.parentElement == this) {
            // if it has an event mask, it's an edit component
            if (comp._editMask) return comp;
            // otherwise it's a mask
            return null;
        }
        // XXX does this case exist?  how can a direct child have a parent element other than its
        // parent?
        return this.deriveSelectedComponent(comp.parentElement);
    },

    // resize selector to current mouse coordinates
    resizeSelector : function () {
        var x = this.getOffsetX(),
            y = this.getOffsetY();

        if (this.selector.keepInParentRect) {
            if (x < 0) x = 0;
            var parentHeight = this.selector.parentElement.getVisibleHeight();
            if (y > parentHeight) y = parentHeight;
        }

        // resize to the distances from the start coordinates
        this.selector.resizeTo(Math.abs(x-this.startX), Math.abs(y-this.startY));

        // if we are above/left of the origin set top/left to current mouse coordinates,
        // otherwise to start coordinates.
        if (x < this.startX) this.selector.setLeft(x);
        else this.selector.setLeft(this.startX);

        if (y < this.startY) this.selector.setTop(y);
        else this.selector.setTop(this.startY);

        // figure out which components are now in the selector hoop
        this.updateCurrentSelection();
    },

    // external, safe getter for selected components
    getSelectedComponents : function () {
        return this.selectedComponents.duplicate()
    }
});


//> @class EditTree
// A TreeGrid that allows drag and drop creation and manipulation of a tree of
// objects described by DataSources.
// <P>
// Nodes can be added via drag and drop from a +link{Palette} or may be programmatically
// added via +link{EditContext.addNode(),addNode()}.  Nodes may be dragged within the tree to reparent
// them.
// <P>
// Eligibility to be dropped on any given node is determined by inspecting the
// DataSource of the parent node.  Drop is allowed only if the parent schema has
// a field which accepts the type of the dropped node.
// <P>
// On successful drop, the newly created component will be added to the parent node under the
// detected field.  Array fields, declared by setting
// <code>dataSourceField.multiple:true</code>, are supported.
// <P>
// An EditTree is initialized by setting +link{EditTree.rootComponent}.  EditTree.data (the
// Tree instance) should never be directly set or looked at.
//
// @treeLocation Client Reference/Tools/EditContext
// @implements EditContext
// @group devTools
// @visibility external
//<



// Class will not work without TreeGrid
if (isc.TreeGrid) {

isc.ClassFactory.defineClass("EditTree", "TreeGrid", "EditContext");

isc.EditTree.addProperties({
    //> @attr EditTree.rootComponent    (Object : null : IR)
    // Root of data to edit.  Must contain the "_constructor" property, with the name of a
    // valid +link{DataSource,schema} or nothing will be able to be dropped on this EditTree.
    // <P>
    // Can be retrieved at any time.
    //
    // @group devTools
    // @visibility external
    //<

    canDragRecordsOut: false,
    canAcceptDroppedRecords: true,
    canReorderRecords: true,

    fields:[
        {name:"ID", title:"ID", width:"*"},
        {name:"type", title:"Type", width:"*"}
    ],

    selectionType:isc.Selection.SINGLE,

    // whether to automatically show parents of an added node (if applicable)
    autoShowParents:true
});

isc.EditTree.addMethods({
    initWidget : function () {
        this.Super("initWidget", arguments);
        this.setData(this.getEditNodeTree());
    },

    // Adding / Removing components in the tree
    // --------------------------------------------------------------------------------------------

    willAcceptDrop : function () {
        if (!this.Super("willAcceptDrop",arguments)) return false;
        var recordNum = this.getEventRow(),
            dropTarget = this.getDropFolder(),
            dragData = this.ns.EH.dragTarget.getDragData()
        ;

        if (dragData == null) return false;
        if (isc.isAn.Array(dragData)) {
            if (dragData.length == 0) return false;
            dragData = dragData[0];
        }

        if (dropTarget == null) dropTarget = this.data.getRoot();
        var dragType = dragData.type || dragData.className;

        this.logInfo("checking dragType: " + dragType +
                     " against dropLiveObject: " + dropTarget.liveObject, "editing");

        return this.canAddToParent(dropTarget, dragType)
    },

    folderDrop : function (nodes, parent, index, sourceWidget) {
        if (sourceWidget != this && !sourceWidget.isA("Palette")) {
            // if the source isn't a Palette, do standard drop interaction
            return this.Super("folderDrop", arguments);
        }

        if (sourceWidget != this) {
            // this causes component creation since the drop is from a Palette
            nodes = sourceWidget.transferDragData();
        }

        var newNode = (isc.isAn.Array(nodes) ? nodes[0] : nodes);

        // flag that this node was dropped by a user
        newNode.dropped = true;

        this.logInfo("sourceWidget is a Palette, dropped node of type: " + newNode.type,
                     "editing");

        var editTree = this;
        this.requestLiveObject(newNode, function (node) {
            if (node == null) return;
            // self-drop: remove component from old location before re-adding
            var selfDrop = sourceWidget == editTree;
            if (selfDrop) {
                // If we're self-dropping to a slot further down in the same parent, this will
                // cause the index to become off by one
                var oldParent = this.data.getParent(newNode);
                if (parent == oldParent) {
                    var oldIndex = this.data.getChildren(oldParent).indexOf(newNode);
                    if (oldIndex != null && oldIndex <= index) index--;
                }
                editTree.removeNode(newNode);
            }

            editTree.addNode(node, parent, index);

            // special case tabs to add default pane
            if (
                !selfDrop && node && parent &&
                (node.type || node.className) == "Tab" &&
                (parent.type || parent.className) == "TabSet"
            ) {
                var liveTabSet = parent.liveObject;
                if (liveTabSet) liveTabSet.editModeAddDefaultPane(node);
            }
        }, sourceWidget)

    },

    nodeAdded : function (newNode) {
        this.selection.selectSingle(newNode);
        if (this.autoShowParents) this.showParents(newNode);

        // Flip it into edit mode depending on the setting on the VB instance

        if (this.creator && this.creator.editingOn) this.enableEditing(newNode);
    },

    // for a node being added without a parent, find a plausible default node to add to.
    // In combination with palette.defaultEditContext, allows double-click (tree, list
    // palettes) as an alternative to drag and drop.
    getDefaultParent : function (newNode, returnNullIfNoSuitableParent) {
        // rules:
        // Start with the selected node. We select on drop / create, so this is typically
        // the last added node, but the user can select something else to take control of
        // where the double-click add goes
        // If this node accepts this type as a child, use that.
        // - handles most layout nesting, DataSource for last form, etc
        // Otherwise, go up hierarchy from this node
        // - handles a series of components that should not nest being placed adjacent instead,
        //   eg ListGrid then DynamicForm
        var type = newNode.type || newNode.className,
            node = this.getSelectedRecord();

        while (node && !this.canAddToParent(node, type)) node = this.data.getParent(node);

        var root = this.data.getRoot()
        if (returnNullIfNoSuitableParent) {
            if (!node && this.canAddToParent(root, type)) return root;
            return node;
        }
        return node || root;
    },

    // give a newNode, ensure all of it's parents are visible
    showParents : function (newNode) {
        // if something is dropped under a tab, ensure that tab gets selected
        var parents = this.data.getParents(newNode),
            tabNodes = parents.findAll("type", "Tab");
        //this.logWarn("detected tab parents: " + tabNodes);
        if (tabNodes) {
            for (var i = 0; i < tabNodes.length; i++) {
                var tabNode = tabNodes[i],
                    tabSetNode = this.data.getParent(tabNode),
                    tab = this.getLiveObject(tabNode),
                    tabSet = this.getLiveObject(tabSetNode);
                tabSet.selectTab(tab);
            }
        }
    }
});

//> @groupDef visualBuilder
// The SmartClient Visual Builder tool is intended for:
// <ul>
// <li> business analysts and others doing functional application design, who want to create
// functional prototypes in a codeless, "what you see is what you get" environment
// <li> developers new to SmartClient who want to get a basic familiarity with component
// layout, component properties and SmartClient code structure
// <li> developers building simple applications that can be completed entirely within Visual
// Builder
// </ul>
// <P>
// <b>Visual Builder for Functional Design</b>
// <P>
// Visual Builder has several advantages over other tools typically used for functional design:
// <ul>
// <li> Visual Builder allows simple drag and drop manipulation of components, form-based
// editing of component properties, and simple connection of events to actions - all without
// requiring any code to be written.  It is actually simpler to use than
// DreamWeaver or other code-oriented prototyping tools
// <li> because Visual Builder generates clean code, designs will not have to be converted to
// another technology before development can proceed.  This reduces both effort and the
// potential for miscommunication
// <li> developers can add custom skinning, components with custom behaviors, and custom
// DataSources with sample datasets to Visual Builder so that the design environment is an even
// closer match to the final application.  This helps eliminate many types of unimplementable
// designs
// <li> because Visual Builder is built in SmartClient itself, Visual Builder is simply a
// web page, and does not require installation.  Visual Builder can be deployed to
// an internal network to allow teams with a mixture of technical and semi-technical
// users to collaboratively build and share prototypes of SmartClient-based applications.
// </ul>
// <P>
// <h4>Launching &amp; Using Visual Builder</h4>
// <P>
// <smartclient>The SmartClient SDK already has Visual Builder installed - access
// it from the SDK Explorer under Tools -&gt; Visual Builder (see QuickStart Guide for how to
// access the SDK Explorer).
// </smartclient>
// <smartgwt>Instructions for launching Visual Builder are in the
// +externalLink{http://forums.smartclient.com/showthread.php?t=8159#aVisualBuilder,Smart GWT FAQ}.
// </smartgwt>
// <P>
// Basic usage instructions are embedded in Visual Builder itself, in the "About Visual
// Builder" pane.  Click on it to open it.
// <P>
// <b>Loading and Saving</b>
// <P>
// The "Project" pane within Visual Builder allows screens to be saved and reloaded for further
// editing.  Saved screens <b>can</b> be edited outside of Visual Builder and successfully
// reloaded, however, as with any design tool that provides a drag and drop, dialog-driven
// approach to screen creation, Visual Builder cannot work with entirely free-form code.  In
// particular, when a screen is loaded and then re-saved:
// <ul>
// <li> any indenting or spacing changes are not preserved
// <li> order of property or method definitions will revert to Visual Builder's default
// <li> while method definitions on components are preserved, any code <b>outside of</b>
//      component definitions will be dropped (in some cases adding such code will cause
//      loading to fail)
// <li> each Canvas-based component will be output separately, in the order these components
//      appear in the project tree, deepest first
// </ul>
// Generally speaking, screen definitions that you edit within Visual Builder should consist of
// purely declarative code.  Rather than appearing in screen definitions, custom components and
// JavaScript libraries should be added to Visual Builder itself via the customization
// facilities described below.
// <P>
// <smartclient>
// <!-- applies only to SmartClient since SmartGWT has a GWT module listing these resources -->
// <h4>Installing Visual Builder</h4>
// <P>
// Visual Builder comes already installed and working in the SDK, and can be used from there out
// of the box.  This is the simplest thing to do during initial prototyping.
// <P>
// Further on in the development cycle, it may be advantageous to have Visual Builder available
// outside the SDK, for example in your test environment.  Installing Visual Builder into
// such an environment is very easy:
// <ul>
// <li>Perform a normal installation procedure, as discussed +link{group:iscInstall,here}</li>
// <li>Copy the following .jar files from the SDK <code>lib</code> folder to the target
// <code>WEB-INF/lib</code> folder:
// <ul>
// <li><code>isomorphic_tools.jar</code></li>
// <li><code>isomorphic_sql.jar</code></li>
// <li><code>isomorphic_hibernate.jar</code></li>
// </ul></li>
// <li>Copy the SDK <code>tools</code> folder to the target application root</li>
// </ul>
// Note that it is safe to include Visual Builder even in a production environment, so long
// as you ensure that the <code>tools</code> folder is protected with any normal HTTP
// authentication/authorization mechanism - for example, an authentication filter.
// </smartclient>
// <P>
// <h4>Customizing Visual Builder</h4>
// <P>
// The rest of this topic focuses on how Visual Builder can be customized and deployed by
// developers to make it more effective as a functional design tool for a particular
// organization.
// <P>
// <b>Adding Custom DataSources to Visual Builder</b>
// <P>
// DataSources placed in the project dataSources directory ([webroot]/shared/ds by default)
// will be detected by Visual Builder whenever it is started, and appear in the DataSource
// listing in the lower right-hand corner automatically.
// <P>
// If you have created a custom subclass of DataSource (eg, as a base class for several
// DataSources that contact the same web service), you can use it with Visual Builder by:
// <ul>
// <li> creating an XML version of the DataSource using the XML tag &lt;DataSource&gt; and the
// <code>constructor</code> property set to the name of your custom DataSource subclass (as
// described +link{group:componentXML} under the heading <i>Custom Components</i>)
// <li> modifying [webroot]/tools/visualBuilder/globalDependencies.xml to load the JavaScript
// code for your custom DataSource class.  See examples in that file.
// </ul>
// <P>
// <b>Adding Custom Components to Visual Builder</b>
// <P>
// The Component Library on the right hand side of Visual Builder loads component definitions
// from two XML files in the [webroot]/tools/visualBuilder directory: customComponents.xml and
// defaultComponents.xml.  customComponents.xml is empty and is intended for developers to add
// their own components.  defaultComponents.xml can also be customized, but the base version
// will change between SmartClient releases.
// <P>
// As can be seen by looking at defaultComponents.xml, components are specified using a tree
// structure similar to that shown in the
// +explorerExample{treeLoadXML,tree XML loading example}.  The properties that can be set on
// nodes are:
// <ul>
// <li> <code>type</code>: name of the SmartClient Class on which +link{Class.create,create()} will be
// called in order to construct the component.  <code>type</code> can be omitted to create
// a folder that cannot be dropped
// <li> <code>title</code>: title for the node
// <li> <code>defaults</code>: an Object specifying defaults to be passed to
// +link{Class.create,create()}.
// For example, you could add an "EditableGrid" node by using <code>type: "ListGrid"</code>
// and specifying:
// <pre>
// &lt;defaults canEdit="true"/&gt;</pre>
// NOTE: if you set any defaults that are not Canvas properties, you need to provide explicit
// type as documented under <i>Custom Properties</i> for +link{group:componentXML}.
// <li> <code>children</code>: components that should appear as children in the tree under this
// node
// <li> <code>icon</code>: icon to show in the Visual Builder component tree (if desired)
// <li> <code>iconWidth/Height/Size</code>: dimensions of the icon in pixels ("iconSize" sets
// both)
// <li> <code>showDropIcon</code>: for components that allow children, whether to show a
// special drop icon on valid drop (like +link{treeGrid.showDropIcons}).
// </ul>
// <P>
// In order to use custom classes in Visual Builder, you must modify
// <code>[webroot]/tools/visualBuilder/globalDependencies.xml</code> to include:
// <ul>
// <li> the JavaScript class definition for the custom class (in other words, the
// +link{classMethod:isc.defineClass(),defineClass()} call)
// <li> a +link{group:componentSchema,component schema} for the custom component
// </ul>
// See globalDependencies.xml for examples.
// <P>
// <h4>Component Schema and Visual Builder</h4>
// <P>
// When you provide +link{group:componentSchema,custom schema} for a component, Visual Builder
// uses that schema to drive component editing (Component Properties pane) and to drive drag
// and drop screen building functionality.
// <P>
// <b>Component Editing</b>
// <P>
// Newly declared fields will appear in the Component Editor in the "Other" category at the
// bottom by default.  You can create your own category by simply setting field.group to the
// name of a new group and using this on multiple custom fields.
// <P>
// The ComponentEditor will pick a FormItem for a custom field by the
// +link{type:FormItemType,same rules} used for ordinary databinding, including the ability to
// set field.editorType to use a custom FormItem.
// <P>
// When the "Apply" button is clicked, Visual Builder will look for an appropriate "setter
// function" for the custom field, for example, for a field named "myProp", Visual Builder will
// look for "setMyProp".  The target component will also be +link{canvas.redraw,redrawn}.
// <P>
// <b>Event -&gt; Action Bindings</b>
// <P>
// The Component Properties pane contains an Events tab that allows you wire components events
// to actions on any other component currently in the project.
// <P>
// Events are simply +link{group:stringMethods,StringMethods} defined on the component.  In
// order to be considered events, method definitions must have been added to the class via
// +link{Class.registerStringMethods} and either be publicly documented SmartClient methods or,
// for custom classes, have a methods definition in the +link{group:componentSchema,component schema}.
// Examples of events are: +link{listGrid.recordClick} and +link{dynamicForm.itemChange}.
// <P>
// Actions are methods on any component that have a method definition in the
// +link{group:componentSchema,component schema} and specify action="true".
// <P>
// All available events (stringMethods) on a component are shown in the Events tab of the
// Component Editor.  Clicking the plus (+) sign next to the event name brings up a menu that
// shows a list of all components currently in the project and their available actions.
// Selecting an action from this submenu binds the action to the selected event.  When an event
// is bound to an action in this manner, automatic type matching is performed to pass arguments
// from the event to the action as follows:
// <ul>
// <li>Only non-optional parameters of the action are bound.
// <li>For each non-optional parameter of the action method, every parameter of the
// event method is inspected in order to either directly match the type (for non-object types)
// or to match an isAssignableFrom type check via a SmartClient schema inheritance check.
// <li>The 'type' of a parameter is determined from the type documented in the SmartClient
// reference for built-in components, or from the <code>type</code> attribute on the method
// param in the +link{group:componentSchema,component schema} definition of a custom component.
// <li>When a matching parameter is found, it is assigned to the current slot of the action and
// not considered for further parameter matching.
// <li>The above pattern is repeated until all non-optional parameters are exhausted, all
// event parameters are exhausted, or until no further type matches can be inferred.
// </ul>
// The "actionBinding" log category can be enabled in the Developer Console to troubleshoot
// issues with automatic binding for custom methods.
// <P>
// <b>Component Drag and Drop</b>
// <P>
// Visual Builder uses component schema to determine whether a given drop is allowed and what
// methods should be called to accomplish the drop.  For example, any Canvas-based component
// can be dropped on a VLayout because VLayout has a "members" field of type "Canvas", and an
// +link{Layout.addMember,addMember()} function.
// <P>
// Because of these rules, any subclass of Canvas will be automatically eligible to be dropped
// into any container that accepts a Canvas (eg, a Layout or Tab).  Any subclass of a FormItem
// will be, likewise, automatically eligible to be dropped into a DynamicForm.
// <P>
// You can declare custom containment relations, such as a custom class "Wizard" that accepts
// instances of the custom class "Pane" by simply declaring a
// +link{group:componentSchema,component schema} that says that Wizard has a property called
// "panes" of type "Pane".  Then, provide methods that allow components to be added and removed:
// <ul>
// <li> for a +link{dataSourceField.multiple,multiple} field, provide "add" and "remove"
// functions based on the name of the field.  For example, for a field "panes" of type "Pane",
// provide "addPane()" that takes a Pane instance, and "removePane()" that takes a pane
// instance or pane ID
// <li> for a singular field (such as +link{Canvas.contextMenu} or +link{Tab.pane}), provide a
// setter method named after the field (eg setContextMenu()) that takes either an instance of
// the component or null for removal
// </ul>
// <P>
// The "editing" log category can be enabled in the Developer Console to troubleshoot issues
// with schema-driven drag and drop and automatic lookup of getter/setter and adder/remover
// methods.
// <P>
// <B>NOTE:</B> after modifying component schema, it may be necessary to restart the servlet
// engine and reload Visual Builder
// <P>
// <b>Presenting simplified components</b>
// <P>
// SmartClient components expose many methods and properties.  For some environments, it is
// more appropriate to provide a simplified list of properties, events, and actions on either
// built-in SmartClient components or your custom components.  This can be done by providing a
// custom +link{group:componentSchema,component schema} for an existing component that exposes
// your minimal set.  You also need to provide a trivial subclass of the class you're exposing
// so that it can be instantiated.
// <P>
// For example, let's say you want to make a simplified button called EButton that exposes only
// the 'title' property and the 'click' event of a standard Button.  The following steps will
// accomplish this:
// <p>
// 1. Edit /tools/visualBuilder/customComponents.xml and add a block similar to the following
// to make your custom component appear in the Component Library:
// <pre>
// &lt;PaletteNode&gt;
//     &lt;title&gt;EButton&lt;/title&gt;
//     &lt;type&gt;EButton&lt;/type&gt;
//     &lt;icon&gt;button.gif&lt;/icon&gt;
// &lt;/PaletteNode&gt;
// </pre>
// 2. Next, create a custom schema: /isomorphic/system/schema/EButton.ds.xml as follows:
// <pre>
// &lt;DataSource ID="EButton" inheritsFrom="Button" Constructor="EButton"
//             showLocalFieldsOnly="true" showSuperClassActions="false"
//             showSuperClassEvents="false"&gt;
//        &lt;fields&gt;
//         &lt;field name="title"  type="HTML"/&gt;
//     &lt;/fields&gt;
//     &lt;methods&gt;
//         &lt;method name="click"&gt;
//             &lt;description&gt;Fires when this button is clicked.&lt;/description&gt;
//         &lt;/method&gt;
//     &lt;/methods&gt;
// &lt;/DataSource&gt;
// </pre>
// See documentation above and also +link{group:componentSchema,component schema} for what the
// properties above do.
// 3.  Finally, you'll need to define an EButton class as a simple subclass of Button, as
// follows:
// <pre>
// isc.defineClass("EButton", "Button");
// </pre>
// To make sure that the Visual Builder will load the above definition, you'll need to place it
// into a JavaScript file being loaded by the Visual Builder.  If you do not already have
// such a file, you can create one and add it to the list of Visual Builder dependencies by
// adding an entry in /tools/visualBuilder/globalDependencies.xml.  See examples in that file
// for specifics.
//
// @see group:toolsDeployment
//
// @treeLocation Concepts
// @title Visual Builder
// @visibility external
//<

} // end if (isc.TreeGrid)


// -----------------------------------------------------------------------------------------
// DynamicForm.rolloverControls

// INCOMPLETE IMPLEMENTATION - commented out for now
/*
isc.DynamicForm.addProperties({
    rolloverControlsLayoutDefaults: [],
    rolloverControls: []

});

isc.DynamicForm.addMethods({
    showRolloverControls : function (item) {
        var controls = this.getRolloverControls(item),
            layout = this.rolloverControlsLayout;
        layout.item = item;
        layout.setPageLeft();
        layout.moveTo(item.getPageLeft()+item.getPageWidth(), item.getPageTop());
    },
    hideRolloverControls : function (item) {
        this.rolloverControlsLayout.hide();
    },
    getRolloverControls : function (item) {
        if (!this.rolloverControlsLayout) {
            this.createRolloverControls(item);
        }

        return this.rolloverControls;
    },
    createRolloverControls : function (item) {
        this.addAutoChild("rolloverControlsLayout");
        this.createRolloverControls(item);
    }
});
*/

// This is a marker class for FormItem drag-and-drop in edit mode.  We use an instance of
// this class (for efficiency, we just keep one cached against the EditContext class) so
// that the DnD code knows we're really dragging a FormItem, which will be present on this
// proxy canvas as property "formItem".
isc.ClassFactory.defineClass("FormItemProxyCanvas", "Canvas");

isc.FormItemProxyCanvas.addProperties({
    autoDraw: false,
    canDrop: true,
    setFormItem : function (formItem) {
        this.formItem = formItem;
        this.syncWithFormItemPosition();
        this.sendToBack();
        this.show();
    },
    syncWithFormItemPosition : function () {
        if (!this.formItem || !this.formItem.form) return; // formItem not yet part of a form?
        this.setPageLeft(this.formItem.getPageLeft());
        this.setPageTop(this.formItem.getPageTop());
        this.setWidth(this.formItem.getVisibleWidth());
        this.setHeight(this.formItem.getVisibleHeight());
    }
});




//> @attr paletteNode.canvasDefaults (Canvas Properties : null : IR)
// @include paletteNode.defaults
// @visibility sgwt
//<
//> @attr paletteNode.formItemDefaults (FormItem Properties : null : IR)
// @include paletteNode.defaults
// @visibility sgwt
//<
//> @attr paletteNode.drawItemDefaults (DrawItem Properties : null : IR)
// @include paletteNode.defaults
// @visibility sgwt
//<
//> @attr paletteNode.canvasLiveObject (Canvas : null : IR)
// @include paletteNode.liveObject
// @visibility sgwt
//<
//> @attr paletteNode.formItemLiveObject (FormItem : null : IR)
// @include paletteNode.liveObject
// @visibility sgwt
//<
//> @attr paletteNode.drawItemLiveObject (DrawItem : null : IR)
// @include paletteNode.liveObject
// @visibility sgwt
//<

//> @attr editNode.canvasDefaults (Canvas Properties : null : IR)
// @include editNode.defaults
// @visibility sgwt
//<
//> @attr editNode.formItemDefaults (FormItem Properties : null : IR)
// @include editNode.defaults
// @visibility sgwt
//<
//> @attr editNode.drawItemDefaults (DrawItem Properties : null : IR)
// @include editNode.defaults
// @visibility sgwt
//<
//> @attr editNode.canvasLiveObject (Canvas : null : IR)
// @include editNode.liveObject
// @visibility sgwt
//<
//> @attr editNode.formItemLiveObject (FormItem : null : IR)
// @include editNode.liveObject
// @visibility sgwt
//<
//> @attr editNode.drawItemLiveObject (DrawItem : null : IR)
// @include editNode.liveObject
// @visibility sgwt
//<



if (!(isc.licenseType == "Enterprise" || isc.licenseType == "Eval" ||
      isc.licenseType == "AllModules" || isc.licenseType.contains("licenseType")))
{

    [
        "EditContext", "Palette", "HiddenPalette", "TreePalette", "ListPalette", "TilePalette",
        "MenuPalette", "EditPane", "EditTree", "FormItemProxyCanvas"
    ].map(function (editModeClass) {
        isc[editModeClass]._vbOnly = true;
    });
}

//> @groupDef toolsDeployment
// SmartClient provides a number of tools:
// <ul>
// <li> +link{group:adminConsole}
// <li> +link{group:visualBuilder}
// <li> +link{group:balsamiqImport}
// </ul>
// <P>
// To deploy the tools simply copy the <code>tools</code> directory into your deployment. There are no
// additional settings to configure.
// <P>
// <h4>Security</h4>
// <P>
// These tools are, by default, available to anyone and enable access to all "BuiltinRPCs"
// and the Filesystem DataSource so they should only be deployed into a trusted environment.
// Alternately, the tools can easily be restricted to administrators or end users
// by protecting the <code>tools</code> path with normal authentication and authorization
// mechanisms on the web server.
// <P>
// More fine-grained access control can be installed by updating each tool's <code>xxxOperations.jsp</code>
// file (ex. tools/adminConsoleOperations.jsp, tools/visualBuilder/vbOperations.jsp). These files are
// responsible for enabling builtinRPC and FileSystem DataSource access. Individual
// BuiltinRPC methods can be restricted, for example, such that some users are allowed to load screens but
// not save any changes. See comments within each file for an example of restricting this access.
// See the server-side Javadocs for methods provided by <code>BuiltinRPC</code>.
// <P>
// Note that the tools provides a "live" interface to the provided DataSources. In
// other words, if a DataSource supports saving and a tool enables editing, real saves will be
// initiated.
//
// @title Tools Deployment
// @treeLocation Concepts/Deploying SmartClient
// @visibility external
//<


// Class will not work without DynamicForm
if (isc.DynamicForm) {

//> @class PropertySheet
//
// Editor with a minimalist appearance, tuned for editing large numbers of properties in a
// constrained space.
//
// @treeLocation Client Reference/Forms
//
// @visibility external
//<
isc.defineClass("PropertySheet", "DynamicForm").addProperties({

    autoChildItems:true,

    // Don't show 'spelling errors' - this is used to edit the properties of form items, etc
    browserSpellCheck:false,

    // for all FormItems
    autoChildDefaults : {
        cellStyle:"propSheetValue",
        titleStyle:"propSheetTitle",
        showHint:false
    },

    // borders don't look particular good around GroupItems
    GroupItemDefaults : {
        cellStyle:null
    },

    ExpressionItemDefaults : {
        width:"*",
        height:18,
        showActionIcon:true
    },
    ActionMenuItemDefaults : {
        width:"*",
        height:18
    },


    SelectItemDefaults : {
        height:20,
        width:"*"
    },

    DateItemDefaults : {
        width:"*"
    },

    TextItemDefaults : {
        width:"*",
        height:20
    },

    StaticTextItemDefaults : {
        width:"*",
        height:20,
        textBoxStyle:"propSheetField"
    },


    ColorItemDefaults : {
        width:"*",
        height:16,
        pickerIconHeight:16, pickerIconWidth:16,
        pickerIconSrc:"[SKIN]/DynamicForm/PropSheet_ColorPicker_icon.png",
        textBoxStyle:"propSheetField"
    },

    HeaderItemDefaults : {
        cellStyle:"propSheetHeading"
    },

    TextAreaItemProperties : {width:"*"},

    // place labels on left
    CheckboxItemDefaults : {
        showTitle:true,
        showLabel:false,
        getTitleHTML : function () { // NOTE: copy of FormItem.getTitle()
            if (this[this.form.titleField] != null) return this[this.form.titleField];
            return this[this.form.fieldIdProperty];
        }
    },

    // Apply a different cellStyle to sectionItems - we don't want the 1px border around them
    SectionItemDefaults : {
        cellStyle:"propSheetSectionHeaderCell"
    },

    titleAlign:"left",
    titleWidth:120,
    cellSpacing:0,
    cellPadding:0,
    backgroundColor:"white",
    requiredTitlePrefix:"<span style='color:green'>",
    requiredTitleSuffix:"</span>",
    titleSuffix:"",
    clipItemTitles:true

});

}


// avoid errors if DataBinding module is loaded without either Grids or Forms
if (isc.ListGrid && isc.DynamicForm) {



//>    @class ListEditor
//
// @implements DataBoundComponent
// @visibility listEditor
//<
isc.defineClass("ListEditor", isc.Layout);

isc.ListEditor.addProperties({
    //> @attr listEditor.inlineEdit (boolean : false : IR)
    // Whether to allow inline editing in the grid.
    // <P>
    // If enabled, the form will show as a modal dialog instead of being displayed side by side
    // with the grid, to prevent ambiguous simultaneous editing within both the grid and form.
    // This mode is suitable for rapid entry/update of records where few properties are
    // normally set.
    //
    // @visibility listEditor
    //<
    //inlineEdit :false,

    //> @attr listEditor.vertical (boolean : false : IR)
    // Whether the form and grid should be shown vertically stacked or horizontally adjacent.
    // <P>
    // Ignored when +link{inlineEdit} is true, since form is then shown in a pop-up.
    //
    // @visibility listEditor
    //<
    vertical:false,

    // Grid subcomponent
    // ---------------------------------------------------------------------------------------
    gridConstructor: isc.ListGrid,
    gridDefaults:{
        editEvent:"click",
        listEndEditAction:"next",
        autoParent:"gridLayout",
        selectionType:isc.Selection.SINGLE,
        recordClick:"this.creator.recordClick(record)",
        editorEnter:"if (this.creator.moreButton) this.creator.moreButton.enable()",
        selectionChanged: function() {
            if (this.anySelected() && this.creator.moreButton) {
                this.creator.moreButton.enable();
            }
        },
        contextMenu : {
            data : [
                {title:"Remove", click: "target.creator.removeRecord()" }
            ]
        }
    },

    // List Buttons
    // ---------------------------------------------------------------------------------------
    gridButtonsDefaults:{
        _constructor:isc.HLayout,
        autoParent:"gridLayout",
        height:10, width:10, layoutMargin:6, membersMargin:10,
        overflow:isc.Canvas.VISIBLE
    },

    newButtonTitle:"New",
    newButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"gridButtons",
        click:"this.creator.newRecord()"
    },

    moreButtonTitle:"More..",
    moreButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"gridButtons",
        click:"this.creator.editMore()",
        disabled:true
    },

    removeButtonTitle:"Remove",
    removeButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"gridButtons",
        click:"this.creator.removeRecord()"
    },

    // Form subcomponent
    // ---------------------------------------------------------------------------------------
    formDefaults:{
        _constructor:isc.DynamicForm,
        autoParent:"formLayout",
        overflow:isc.Canvas.AUTO
    },

    // Form Buttons
    // ---------------------------------------------------------------------------------------
    formButtonsDefaults:{
        _constructor:isc.HLayout,
        autoParent:"formLayout",
        height:10, width:10, layoutMargin:6, membersMargin:10,
        overflow:isc.Canvas.VISIBLE
    },

    saveButtonTitle:"Save",
    saveButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"formButtons",
        click:"this.creator.saveRecord();"
    },

    cancelButtonTitle:"Cancel",
    cancelButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"formButtons",
        click:"this.creator.cancelChanges()"
    },

    resetButtonTitle:"Reset",
    resetButtonDefaults:{
        _constructor:isc.AutoFitButton,
        autoParent:"formButtons",
        click:"this.creator.form.resetValues()"
    },

    // Sublayouts
    // ---------------------------------------------------------------------------------------
    gridLayoutDefaults : {
        _constructor:isc.VLayout
    },

    gridButtonsOrientation:"left",

    formLayoutDefaults : {
        _constructor:isc.VLayout,
        autoFocus:true
    },

    animateMembers:true,
    membersMargin:10,

    // Changes dialog
    // ---------------------------------------------------------------------------------------
    confirmLoseChangesMessage:"Discard changes?",


    // AutoChildren
    // ---------------------------------------------------------------------------------------
    draw : function () {
        if (isc._traceMarkers) arguments.__this = this;

        if (!this.readyToDraw()) return this;

        return this.Super("draw", arguments);
    },

    initWidget : function () {
        this.Super("initWidget", arguments);
        // don't show the edit button by default if we're allowing inline editing, since
        // just clicking triggers editing
        if (!this.inlineEdit) this.showMoreButton = this.showMoreButton || false;

        this.addAutoChild("gridLayout");
        this.addAutoChild("grid", { _constructor: this.gridConstructor } );
        this.addAutoChildren(this.gridButtonsGroup);
        this.addAutoChildren(this.formGroup);

    },

    formGroup : [
        "formLayout", "form", "formButtons", "saveButton", "cancelButton", "resetButton"
    ],
    gridButtonsGroup : [
        "gridButtons", "newButton", "moreButton"
    ],

    configureAutoChild : function (child, childName) {
        if (isc.isA.Button(child)) child.title = this[childName + "Title"];

        if (child == this.grid) {
            child.dataSource = this.dataSource;
            child.fields = this.fields;
            child.saveLocally = this.saveLocally;
            child.canEdit = this.inlineEdit;
        }

        if (this.gridButtonsOrientation == isc.Canvas.RIGHT) {
            // place buttons to right of list
            if (child == this.gridLayout) child.vertical = false;
            if (child == this.formLayout) child.vertical = false;
            // stack buttons vertically
            if (child == this.gridButtons) child.vertical = true;
            if (child == this.formButtons) child.vertical = true;
        }

        if (child == this.form) {
            child.dataSource = this.dataSource;
            child.fields = this.formFields;
        }
        if (this.inlineEdit) {
            if (child == this.formLayout) child.visibility = isc.Canvas.HIDDEN;
        } else {
            if (child == this.gridLayout) child.showResizeBar = true;
        }
    },

    // DataSources and Data
    // ---------------------------------------------------------------------------------------

    setDataSource : function (dataSource, fields) {
        this.dataSource = dataSource || this.dataSource;
        if (this.grid != null) {
            this.grid.setDataSource(dataSource, fields);
            this.form.setDataSource(dataSource, fields);
        }
    },
    setData : function (data) {

        if (data == null) data = [];

        if (data.dataSource) this.setDataSource(data.dataSource);
        if (this.grid != null) {
            this.grid.setData(data);
            this.form.clearValues();
        } else {
            isc.addProperties(this.gridDefaults, this.gridProperties || {}, {data:data});
        }
    },
    getData : function () {
        // on a getData call, always save the current edit to the dataSet before returning
        if (this.inlineEdit) this.grid.endEditing();
        return this.grid.getData();
    },

    // Button / Menu actions
    // ---------------------------------------------------------------------------------------

    // cancel button while editing in form
    cancelChanges : function () {
        this.form.clearValues();
        this.showList();
    },

    showList : function () {
        if (this.inlineEdit) {
            this.formLayout.animateHide({effect:"wipe", startFrom:"R"});
            this.gridLayout.animateShow({effect:"wipe", startFrom:"R"});
        }
    },
    showForm : function () {
        if (this.inlineEdit) {
            this.gridLayout.animateHide({effect:"wipe", startFrom:"R"});
            this.formLayout.animateShow({effect:"wipe", startFrom:"R"});
        }
    },

    // edit via form on recordClick in the list, unless inline editing is allowed
    recordClick : function (record) {
        if (this.inlineEdit) return;


        var _this = this;

        var proceed  = function (ok) {
            if (ok) {
                _this.currentRecord = record;
                if (!_this.inlineEdit) _this.form.editRecord(record);
                _this.form.setValues(isc.addProperties({}, _this.grid.getSelectedRecord()));
            }
        }

        // editing in parallel form: if there are changes, pop up a warning that this will
        // abandon changes to the currently edited item
        if (!this.form.valuesHaveChanged()) proceed(true);
        else this.confirmLoseChanges(proceed);
    },

    getEditRecord : function () {
        var editRowNum = this.grid.getEditRow();
        if (editRowNum != null) {
            return this.grid.getEditedRecord(editRowNum);
        } else {
            return isc.addProperties({}, this.grid.getSelectedRecord());
        }
    },

    // More... button, inlineEdit only
    editMore : function () {
        this.currentRecord = this.getEditRecord();
        this.showForm();
        this.form.setValues(this.currentRecord);
    },

    newRecord : function () {
        if (this.inlineEdit) return this.grid.startEditingNew();

        var _this = this;

        var proceed = function (ok) {
            if (ok) {
                _this.grid.deselectAllRecords();
                _this.showForm();
                _this.form.editNewRecord();
            }
        }

        // editing in parallel form: if there are changes, pop up a warning that this will
        // abandon changes to the currently edited item
        if (!this.form.valuesHaveChanged()) proceed(true);
        else this.confirmLoseChanges(proceed);
    },

    // remove record context click
    removeRecord : function () {
        this.form.clearValues();
        this.grid.removeSelectedData();
    },
    saveRecord : function () {
        if (!this.form.validate()) return false;
        var values = this.form.getValues();

        this.showList();

        if (this.form.saveOperationType == "add") { // new record
            this.grid.addData(values);
        } else {
            // if inline editing is occurring just apply the values as editValues.  We can't
            // count on updateData() because we may have edited a new row that doesn't have a
            // primary key
            if (this.inlineEdit && this.grid.getEditRow() != null) {
                var rowNum =this.grid.getEditRow();

                if (this.grid.data[rowNum] != null) this.grid.updateData(values)
                else this.grid.setEditValues(rowNum, values);
            } else {
                this.grid.updateData(values);
            }
        }

        return true;
    },

    confirmLoseChanges : function (callback) {
        isc.confirm(this.confirmLoseChangesMessage, callback);
    },

    //
    // ---------------------------------------------------------------------------------------
    validate : function () {
        if (this.form.isVisible() && this.form.valuesHaveChanged()) {
            return this.form.validate();
        }
        return true;
    }
});


} // end if (isc.ListGrid && isc.DynamicForm)







//>    @class    ViewLoader
//
// The ViewLoader component can be used to load new SmartClient-based user interfaces into a
// running application.
// <P>
// <b>NOTE:</b> before using a ViewLoader, be sure that you have read about and understood the
// +link{group:smartArchitecture,SmartClient Architecture}.  The most responsive and
// scalable application architecture preloads views rather than using ViewLoaders.
// <P>
// A ViewLoader is a Canvas, and can be provided anywhere a Canvas can be provided: as a Tab
// pane, and Layout member, etc.  When a ViewLoader draws, it shows a
// +link{viewLoader.loadingMessage,loading message}, then
// performs an RPC to the +link{viewLoader.viewURL,viewURL} to load components.
// <P>
// The response from the viewURL should be SmartClient components defined in JavaScript, with no
// surrounding &lt;SCRIPT&gt; tags or other HTML framing.  The returned script can be
// dynamically generated, for example, it may be the result of a JSP containing an XML view
// description enclosed in +link{group:xmlTag,&lt;isomorphicXML&gt;} tags.
// <P>
// In the returned script, the special variable "viewLoader" is available to refer to the
// ViewLoader instance that is loading components.  The intended usage is that the returned
// script creates a view consisting of SmartClient components, then calls
// <code>viewLoader.setView(myView)</code> to place the loaded view into the ViewLoader.
// If the view does not call setView() explicitly, the viewLoader will find the last top-level
// UI component (Canvas subclass) created by the view and set that as the current view.
// Top-level in this case means that the UI component is not contained in another UI component
// as a member or child.
// <p>
// The ViewLoader relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
//
// @see rpcRequest.evalResult
// @see group:smartArchitecture for general discussion of architectural best practices in
//      SmartClient
// @group viewLoading
// @treeLocation Client Reference/Foundation
// @visibility external
// @example viewLoading
//<



// NOTE: we are a subclass of Label as a means of showing the loading message
isc.ClassFactory.defineClass("ViewLoader", isc.Label);

isc.ViewLoader.addProperties({

//> @attr viewLoader.loadingMessage  (HTMLString : "Loading View...&nbsp;${loadingImage}" : IR)
// Message to show while the view is loading.
// Use <code>"&#36;{loadingImage}"</code> to include +link{Canvas.loadingImageSrc,a loading image}.
//
// @group viewLoading
// @visibility external
//<
loadingMessage:"Loading View...&nbsp;${loadingImage}",
align:isc.Canvas.CENTER,
allowContentAndChildren:true,

//> @attr viewLoader.viewURL         (URL : null : IR)
// URL to load components from.
//
// @visibility external
// @example viewLoading
//<
//viewURL: null,

//> @attr viewLoader.viewURLParams   (Object : null : IR)
// Parameters to be sent to the viewURL when fetching the view.
// @visibility external
//<
//viewURLParams: null,

//> @attr viewLoader.viewRPCProperties (RPCRequest Properties : null : IRA)
// RPCRequest properties to be sent with every RPCRequest issued by the ViewLoader.  Very
// advanced; could be used to, for example, set HTTP headers.
// @visibility external
//<

//> @attr viewLoader.httpMethod         (String : "GET" : IRW)
// Selects the HTTP method that will be used when fetching content.  Valid values are "POST"
// and "GET".
// @group contentLoading
// @visibility external
//<
httpMethod:"GET", // default would POST

useSimpleHttp:true, // don't send stuff that RPC layer usually sends (serialized transaction, etc)

//> @attr viewLoader.allowCaching (Boolean : false : IR)
// By default a ViewLoader will explicitly prevent browser caching.
// <P>
// Set to true to allow browser caching <b>if the browser would normally do so</b>, in other
// words, if the HTTP headers returned with the response indicate that the response can be
// cached.
//
// @visibility external
//<

transformXML: true, // transform .xml file contents via client-side XML parser?

// so that we get allocated space in Layouts, instead of autoFitting
overflow:"hidden"
});

isc.ViewLoader.addMethods({

initWidget : function () {
    this.Super(this._$initWidget);

    // if we've been given a placeholder widget, add it
    if (this.placeholder) this.addChild(this.placeholder);
    // otherwise show the loading message
    else this.contents = this.getLoadingMessage();
},

draw : function () {
    if (!this.readyToDraw()) return this;
    this.Super("draw", arguments);

    if (this.view) {
        this.addChild(this.view);
        this.view.show();
    } else if (this.viewURL && !this.loadingView()) {
        // fetch from server if so configured
        this.setViewURL();
    }
    return this;
},

// simple layout policy just fills the view
layoutChildren : function () {
    this.Super("layoutChildren", arguments);
    var children = this.children;
    if (!children || children.length == 0) return;

    var child = this.children[0],
        width = this.getWidth(),
        height = this.getHeight();

    // don't resize a loaded view that has specific sizes set on it
    if (child._userWidth != null) width = null;
    if (child._userHeight != null) height = null;

    // NOTE: we intentionally occlude styling such as borders, if any, which are only meant to
    // exist while we are showing the loading message
    child.setRect(0, 0, width, height);
},

destroy : function () {
    if (this.placeholder) this.placeholder.destroy();
    if (this.view) this.view.destroy();
    this.Super("destroy", arguments);
},

// dynamically sets a custom placholder
setPlaceholder : function (placeholder) {
    if (this.placeholder) this.placeholder.destroy();
    this.placeholder = placeholder;
    this.addChild(placeholder);
    this.placeholder.sendToBack();
},


//> @method viewLoader.setViewURL()
// Change the URL this component loads a view from.  Triggers a fetch from the new URL.
// <P>
// Can also be called with no arguments to reload the view from the existing
// +link{viewLoader.viewURL}.
//
// @param [url]    (URL)     URL to retrieve view from
// @param [params] (Object)  Parameters to send to the viewURL.  Merged with
//                           <code>component.viewURLParams</code> if both are set.
// @param [rpcProperties] (RPCRequest Properties)  Additional properties for the RPCRequest
//                        sent by the ViewLoader.  Very advanced; could be used to, for
//                        example, set HTTP headers.
// @group viewLoading
// @visibility external
//<
setViewURL : function (url, params, rpcProperties) {
    if (url != null) this.viewURL = url;
    url = this.viewURL;

    if (this.placeholder) {
        this.placeholder.show();
        this.placeholder.bringToFront();
    }
    // change contents back to loading message on reload
    if (this.view != null) {
        this.view.hide();
        this.setContents(this.getLoadingMessage());
    }

    var baseParams = {},
        useSimpleHttp = this.useSimpleHttp,
        httpMethod = this.httpMethod,
        evalResult = false;

    // normally we send an XMLHttpRequest and expect a pure JavaScript response, however if ActiveX
    // is off in IE, we can't use XMLHttpRequest, so we send a normal, frames-based RPC instead.
    // This means whatever is at the viewURL must use the server-side RPCManager APIs and send the data
    // back as a String.
    if (!isc.rpc.xmlHttpRequestAvailable()) {
        this.logInfo("XMLHttpRequest not available, using frames comm and expecting RPCResponse");
        baseParams = { };
        useSimpleHttp = false; // useSimpleHttp: true not supported for frames comm
        httpMethod = "POST";
        evalResult = false;
    }

    var reloadRequest = isc.addProperties({
        showPrompt: false,
        actionURL: this.viewURL,
        httpMethod: httpMethod,
        useSimpleHttp: useSimpleHttp,
        // IE caches very aggressively by default, which can be confusing, so disable caching
        // unless this (currently undocumented) flag is set
        bypassCache: !this.allowCaching,
        params: isc.addProperties(baseParams, this.viewURLParams, params)
    },
    this.viewRPCProperties, // widget rpc properties
    rpcProperties, // method rpc properties
    // non-overridable
    {
        evalResult: evalResult,
        suppressAutoDraw: true,
        willHandleError: true,
        callback: "if(window."+this.getID()+")"+this.getID()+"._loadViewReply(rpcRequest, rpcResponse, data)"
    });

    // add this component to evalVars
    if (!reloadRequest.evalVars) reloadRequest.evalVars = {};
    reloadRequest.evalVars.viewLoader = this;

    this._loadNumber = isc.rpc.sendProxied(reloadRequest, true).transactionNum;
},

loadingView : function () {
    return this._loadNumber != null;
},

_loadViewReply : function (rpcRequest, rpcResponse, data) {
    // handles case of setViewURL() being called while we're fetching from some other URL
    // ignore all but the most recent load
    if (rpcRequest.transactionNum != this._loadNumber) {
        return;
    }

    delete this._loadNumber;

    this._viewSet = false;

    if (rpcResponse.status != isc.RPCResponse.STATUS_SUCCESS) {
        if (this.handleError(rpcRequest, rpcResponse) === false) return;
    }

    try {
        // if the target file was an xml file, then components are assumed to be in XML format,
        // client-transalteable via toComponents().  Ideally we would have a toComponents() mode
        // that would return the JS for the passed XML so we can follow the globalEvalWithCapture
        // path below and support evalVars.  In the absense of that, we do the "last top-level
        // canvas is the view".
        //
        // Since in the XML case we don't have the list of created globals (since we don't
        // globalEvalWithCapture()), we look at isc.Canvas._canvasList to find the same information
        if (rpcRequest.actionURL.endsWith(".xml") && this.transformXML) {
            var _canvasList = isc.Canvas._canvasList;

            // fixate the length of the canvasList prior to creating the new view - everything
            // after this index are the new components that we'll search for the view
            var lastIndex = _canvasList.length;

            isc.xml.toComponents(data);

            if (!this._viewSet) {
                for (var i = _canvasList.length; i >= lastIndex; i--) {
                    var obj = _canvasList[i];
                    if (obj != null && isc.isA.Canvas(obj) &&
                        obj.parentElement == null && obj.masterElement == null)
                    {
                        this.setView(this.transformView(obj));
                        break;
                    }
                }
            }

            // complete view loading
            this._loadViewReplyComplete();

        } else {
            var viewLoader = this;
            isc.Class.globalEvalWithCapture(data, function (globals, error) {
                isc.Log.logWarn("firing the callback from global eval with...");
                isc.Log.logWarn('viewLoader is:' + viewLoader);
                if (error) {
                    viewLoader.handleError(rpcRequest, rpcResponse, error);
                } else {
                    viewLoader._loadViewReplyComplete(globals);
                }
            }, rpcRequest.evalVars);
        }
    } catch (e) {
        this.handleError(rpcRequest, rpcResponse, e);
    }
},

_loadViewReplyComplete : function (globals) {

    if (!this._viewSet && globals) {
        // if view we just loaded didn't call setView(), automatically find the last top-level
        // Canvas in the response and set that as the view.
        //
        // Note: globalEvalWithCapture return globalIDs in the order they were created.
        // Typically the top-level container is declared last since it incorporates other
        // Canvii declared before it, so we count down from the last created Canvas here.
        for (var i = globals.length; i >= 0; i--) {
            var global = globals[i];
            var obj = window[global]; // globals are IDs, dereference
            if (obj && isc.isA.Canvas(obj) &&
                obj.parentElement == null && obj.masterElement == null)
            {
                this.setView(this.transformView(obj));
                break;
            }
        }
    }

    if (!this._viewSet) {
        this.logWarn("setView() not explicitly called by loaded view and could"
                     + " not be autodetected for view: " + this.getID());
    }

    this.viewLoaded(this.view); // notify observers
},

// end-user hook to last minute transform betfore we set the view
transformView : function (obj) {
    return obj;
},


//> @method viewLoader.handleError()
//
// This method is called when a transport error occurs.  Typically, this is the result of the
// server returning an HTTP error code such as 404 - document not found.  You can inspect the
// RPCResponse object for the reasons for the error and take appropriate action.  Typical
// properties to look at are rpcResponse.status, and rpcResponse.httpResponseCode.
// <p>
// This method is called from the response processing pipeline.  If you want to provide your
// own HTML response as the result of the error, you can do
// so by setting rpcResponse.data to your HTML string.  Returning false from this method
// suppresses any further response handling.  The default implementation of this method causes
// an error message to be logged to the Developer Console and sets the HTML to the error
// string.
//
// @param rpcRequest    (RPCRequest)      The RPCRequest that was made to the server
// @param rpcResponse   (RPCResponse)     The RPCResponse that was received
//
// @return (boolean) false to suppress further response processing
//
// @group contentLoading
// @visibility external
//<
handleError : function (rpcRequest, rpcResponse, jsError) {
    this.logWarn("ViewLoader received bad response:\n" + isc.echo(rpcResponse.data));
    this.setView(isc.Label.create({
        contents: jsError ? jsError.toString() : rpcResponse.data
    }));
    return false;
},


setView : function (view) {
    if (view != null && view == this.view) return;

    this._viewSet = true;
    this.setContents("&nbsp;");

    if (this.view) this.view.destroy();
    this.view = view;

    if (view == null) return;

    // add the view as a child, suppressing drawing until we have a chance to size it
    this.addChild(view, null, false);
    this.layoutChildren();
    view.draw();
    this.logInfo("showing view: " + view);

    if (this.placeholder) this.placeholder.hide();
    // hide loading message
    this.contents = "&nbsp;";
},

//> @method viewLoader.getView()
// Retrieve the current view.  May be null if the view has not yet been loaded, or has been
// explicitly set to null.
//
// @return (Canvas) the current view
//
// @group contentLoading
// @visibility external
//<
getView : function () {
    return this.view;
},

//> @method viewLoader.viewLoaded()
// StringMethod fired when the view has been loaded.  Has no default implementation.  May be
// observed or overridden to fire custom logic when loading completes.
//
// @param view (Canvas) the view that was loaded
//
// @group contentLoading
// @visibility external
//<
viewLoaded : function (view) {
    // observable/overrideable
},

getLoadingMessage : function () {
    return this.loadingMessage == null ? "&nbsp;" : this.loadingMessage.evalDynamicString(this, {
        loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                   isc.Canvas.loadingImageSize,
                                   isc.Canvas.loadingImageSize)
        });
}
});






// HTMLFlow / HTMLPane
// ---------------------------------------------------------------------------------------
// Behaviors in these classes are slated to be moved down to Canvas.  Theses class principally
// exist as a way of organizing functionality for more useful defaults, documentation, tool and
// skinning purposes.

//>    @class    HTMLFlow
//
// Use the HTMLFlow component to display HTML content that should expand to its natural size
// without scrolling.
// <p>
// HTML content can be loaded and reloaded from a URL via the property
// <code>contentsURL</code>.  This method of loading is for simple HTML content
// only; SmartClient components should be loaded via the +link{class:ViewLoader} class.
// <P>
// NOTE: Since the size of an HTMLFlow component is determined by its HTML contents, this
// component will draw at varying sizes if given content of varying size.  When using HTMLFlow
// components within a Layout, consider what will happen if the HTMLFlow renders at various
// sizes.  An HTMLFlow which can expand should be placed in a container where other components
// can render smaller, where the container is allowed to scroll, or where there is padding to
// expand into.
//
// @group contentLoading
// @treeLocation Client Reference/Foundation
// @visibility external
// @example htmlFlow
//<
isc.ClassFactory.defineClass("HTMLFlow", "Canvas");

isc.HTMLFlow.addClassMethods({



//> @classMethod htmlFlow.executeScript() (A)
// Given a block of HTML extract any JavaScript from it (including script loaded from external
// files via <code>script src=...</code> tags), and execute it.<br>
// Note: This method will be asynchronous if script blocks needs to be loaded from the server.
// @param html (HTML) Block of HTML from which script tags will be extracted and executed
// @param callback (callback) Callback to fire when the script has been executed. If a
//  script error occurred during execution, it will be passed to this parameter as the
//  first parameter <code>error</code>
// @param [displayErrors] (boolean) By default script errors encountered executing the
//  extracted script will be logged to the developer console. Pass in a false value for this
//  parameter to suppress this behavior
// @visibility internal
//<
// Not currently used anywhere
_scriptID:0,
_executeContext:[],
executeScript : function (html, callback, displayErrors) {
    this._executeContext[this._scriptID] = {callback:callback, displayErrors:displayErrors};
    this._scriptID++;
    this.getScript(
        html,
        "isc.HTMLFlow._completeExecuteScript(" + this._scriptID + ",htmlFragments, scripts);"
    );
},

// helper for asynchronous executeScript() method
_completeExecuteScript : function (scriptID, htmlFragments, scripts) {
    var context = this._executeContext[scriptID];
    delete this._executeContext[scriptID];
    // reset scriptID to 0 if we've got no other outstanding executions so we dont let
    // the array get large
    var empty=true;
    for (var i = 0; i < this._executeContext.length; i++) {
        if (this._executeContext[i] != null) {
            empty = false;
            break;
        }
    }
    if (empty) this._scriptID = 0;

    isc.Class.globalEvalWithCapture(scripts, context.callback, null, context.displayErrors);
},


//> @classMethod htmlFlow.getScript() (A)
// Retrieves any JavaScript embedded in a snippet of HTML as a string of script that
// can be executed.
// <P>
// If the HTML contains a <code>&lt;script src=...</code> tag, this method will load that external
// script file, extract its contents and included it in the returned block of script.
// <P>
// Note that since the script may have to be loaded from the server, this method may be
// asynchronous.
//
// @param html (HTML) Snippet of HTML to retrieve the script from
// @param callback (callback) callback fired when the script has been retrieved. Parameters
//    passed to this callback are <code>html</code> [the block of html] and <code>script</code>
//    [the script].
// @param [extractScriptFromHTML] (boolean) Should the <code>html</code> parameter passed to
//    the callback include the extracted script blocks, or have them removed.
// @visibility internal
//<
// Used by _loadContentReply
getScript : function (html, callback, extractScriptFromHTML, dontFetchScripts) {
    // Make a copy of the HTML for us to return if extractScriptFromHTML if false
    var originalHTML = html;

    // Strip all <!-- --> comment blocks from the html before extracting script tags so we
    // only get script that would natively execute

    var commentStart, commentEnd,
        commentStartIndex, commentEndIndex;

    while ((commentStart = html.match(/<!--/i)) != null) {
        commentEnd = html.match(/-->/i);
        if (commentEnd == null || (commentEnd.index < commentStart.index)) {
            this.logWarn('HTMLFlow content contains an opening comment tag "<!--"' +
                    ' with no closing tag "-->", or vice versa. We recommend you review this ' +
                    'HTML (original HTML follows):\n'
                    + originalHTML);
            // strip the mismatched comment tag from the working html as if it were a
            // full comment block
            if (commentEnd) {
                commentStartIndex = commentEnd.index;
                commentEndIndex = commentStartIndex + 3;
            } else {
                commentStartIndex = commentStart.index;
                commentEndIndex = commentStartIndex + 4;
            }
        } else {
            commentStartIndex = commentStart.index;
            commentEndIndex = commentEnd.index + 3;
        }
        html = html.slice(0, commentStartIndex) + html.slice(commentEndIndex, html.length);
    }


    // parse out <SCRIPT> blocks to eval later.
    var scripts = []; // script accumulator
    var scriptIncludes = []; // src= values

    var htmlFragments = [];
    var htmlRemaining = html;
    html = null;

    var scriptStart;

    // match the start of a <script> block: note that the script block can contain a type or
    // language or type specifier or a src=
    while ((scriptStart = htmlRemaining.match(/(<script([^>]*)?>)/i)) != null) {
        var scriptStartTag = scriptStart[1];

        // chop out the opening script tag, then pick up the closing script tag.
        // If we hit another opening script tag before the closing tag, or can't find a
        // closing tag, log a warning and continue.
        htmlFragments.add(htmlRemaining.slice(0, scriptStart.index));
        scripts.add(null);
        scriptIncludes.add(null);
        htmlRemaining = htmlRemaining.slice(
            scriptStart.index+ scriptStartTag.length, htmlRemaining.length)

        // match the end of the script block
        var scriptEnd = htmlRemaining.match(/<\/script>/i),
            nextScriptStart = htmlRemaining.match(/(<script([^>]*)?>)/i);

        if (scriptEnd == null || (nextScriptStart && (scriptEnd.index > nextScriptStart.index))) {
            this.logWarn("HTMLFlow content contains an opening <script ...> tag " +
                    "with no closing tag, or vice versa. Stripping out this tag:" + scriptStartTag);
            continue;
        }

        // the script block is the stuff inside the <script></script> brackets
        var scriptBlock = htmlRemaining.slice(0, scriptEnd.index);

        // pull the script block we just matched out of the html stream so we can match
        // additional script blocks and because it's possible that rendering the script blocks
        // will get the executed - which we don't want because we'll be evaling them ourselves.
        htmlRemaining = htmlRemaining.slice(scriptEnd.index+9, htmlRemaining.length);

        // only append script blocks containing JS code.  A js script block either does not
        // specify type/language, or has a type=/language= that
        // contains javascript, jscript, or ecmascript
        var isJS = (scriptStartTag.match(/<script\s*(language|type)/i) == null) ||
                   (scriptStartTag.match(/<script\s*(language|type)\s*=["']?[^'"]*(javascript|ecmascript|jscript)[^'"]*["']?/i)
                     != null);

        // override point for excluding scripts
        if (!this.shouldLoadScript(scriptStartTag)) continue;

        if (isJS) {
            var srcMatch;
            if (srcMatch = scriptStartTag.match(/src=('|")?([^'"> ]*)/i)) {
                scriptIncludes.add(srcMatch[2]);
                scripts.add(null);
            } else {
                // avoid empty script blocks which can lead to hangs
                if (!isc.isA.String(scriptBlock) || isc.isAn.emptyString(scriptBlock)) continue;
                // slot this script into our script accumulator
                scripts.add(scriptBlock);
                scriptIncludes.add(null);
            }
            htmlFragments.add(null);
        } else {
            // Warn (rather than just logInfo) if we hit non JS script tags since functionality
            // could actually be lost from the HTML in some cases
            this.logWarn("html to be evaluated contains non-JS script tags - these will be"
                         + " ignored.  Tag: " + scriptStartTag);
        }
    }

    // if scriptStart never matched, set htmlFragments to the entire html text
    if (htmlFragments.length == 0)
        htmlFragments = [ htmlRemaining ];
    else
        htmlFragments.push(htmlRemaining);

    // If we had any 'script src=...' tags, we need to load their contents and fold it into
    // the script to evaluate

    if (scriptIncludes.length > 0 && !dontFetchScripts) {
        if (isc.RPCManager) {
            var loadingScripts = false;
            for (var i = 0; i < scriptIncludes.length; i++) {
                // it's a sparse array
                if (scriptIncludes[i] == null) {
                    continue;
                }
                isc.RPCManager.sendRequest(
                    {actionURL: scriptIncludes[i], serverOutputAsString:true, httpMethod:"GET",
                     internalClientContext:{
                        scriptIndex:i, scripts:scripts, scriptIncludes:scriptIncludes,
                        callback:callback,
                        htmlFragments:(extractScriptFromHTML ? htmlFragments : [originalHTML])
                     },
                     callback:"isc.HTMLFlow.loadedRemoteScriptBlock(data, rpcResponse.internalClientContext)"
                    }
                );
                loadingScripts = true;
            }
            // wait for the script files to be loaded (asynchronous) before firing the
            // 'completion' method
            if (loadingScripts) return;
        } else {
            // Warn the user that their non-JS code and JS code loaded via SCRIPT SRC= will not execute.
            this.logWarn("html contains <script src=> blocks with the "
                        +"following target URLs: " + scriptIncludes + " If you want "
                        +"these to be dynamically loaded, please include the "
                        +"DataBinding module or include the contents of "
                        +"these files in inline <script> blocks.");
        }
    }
    // In this case we have no script src =, so we can synchronously fire the callback
    var script = scripts.join("\n");

    this.fireCallback(callback, "htmlFragments,dontFetchScripts,scripts",
        [(extractScriptFromHTML ? htmlFragments : [originalHTML]),
        dontFetchScripts,
        scripts]);
},

// whether we should load a script based on the <SCRIPT> tag (passed as first argument)
shouldLoadScript : function (scriptStartTag) {
    // skip loading SmartClient modules that are already loaded
    var iscMatch = scriptStartTag.match(/ISC_([^.]*)\.js/i);
    if (iscMatch && isc["module_" + iscMatch[1]]) return false;

    // skip loading other skins or loading the skin twice.  Among many other problems, loading
    // a skin twice could change defaults like showEdges, causing components to assume they have
    // edges that they don't.
    var iscMatch = scriptStartTag.match(/load_skin\.js/i);
    if (iscMatch) return false;

    return true;
},

// Helper for 'getScript()' to fold the contents of (asynchronously loaded) javascript files
// into any inline script blocks encountered in html passed to that method.
loadedRemoteScriptBlock : function (script, context) {
    var scriptIndex = context.scriptIndex,
        scripts = context.scripts,
        scriptIncludes = context.scriptIncludes;

    scripts[scriptIndex] = script;
    delete scriptIncludes[scriptIndex];

    // Check whether we've loaded all outstanding scripts from the original request
    for (var i = 0; i < scriptIncludes.length; i++) {
        if (scriptIncludes[i] != null) return;
    }
    // dontFetchScripts always is false
    this.fireCallback(context.callback, "htmlFragments,dontFetchScripts,scripts",
        [context.htmlFragments,
        false,
        scripts]);
}


});

isc.HTMLFlow.addProperties({

defaultWidth:200,
defaultHeight:1,

// both children and content will exist when both HTML code and SmartClient script blocks
// exist in the contents
allowContentAndChildren : true,

// enable text selection and i-beam cursor for HTMLFlow and HTMLPane contents
cursor:"auto",


//>    @attr htmlFlow.contents        (HTMLString : "&nbsp;" : [IRW])
// @include canvas.contents
//<

//> @attr htmlFlow.dynamicContents (Boolean : false : IRWA)
//    @include canvas.dynamicContents
//<

//> @attr htmlFlow.contentsURL    (URL : null : IRW)
// URL to load content from.
// <P>
// If specified, this component will load HTML content from the specified URL when it is
// first drawn.
// <p>
// This feature relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
//
// @group contentLoading
// @visibility external
//<
// NOTE: to load content before draw, or refresh contents from the server after draw
// setContentsURL() can be called manually.

//> @attr htmlFlow.loadingMessage      (HTMLString : "&nbsp;${loadingImage}" : IRW)
// HTML to show while content is being fetched, active only if the <code>contentsURL</code>
// property has been set.
// Use <code>"&#36;{loadingImage}"</code> to include +link{Canvas.loadingImageSrc,a loading image}.
// <P>
// The loading message will show both during the initial load of content, and during reload if
// the contents are reloaded or the contentsURL changed.  For a first-time only loading
// message, initialize the <code>contents</code> property instead.<br>
// Note: the <code>loadingMessage</code> is never displayed when loading complete web pages
// rather than HTML fragments (see +link{HTMLFlow.contentsType}).
//
// @group contentLoading
// @visibility external
//<
// NOTE: no setter, intended usage is to setLoadingMessage then call setContentsURL()
loadingMessage: "&nbsp;${loadingImage}",

//> @attr htmlFlow.contentsURLParams   (Object : null : IRW)
// Parameters to be sent to the contentsURL when fetching content.
// @group contentLoading
// @visibility external
//<
// Intended usage: specifying something like a chartId for a dynamically loaded and reloaded
// server-generated chart.
//contentsURLParams: null,

//> @attr htmlFlow.httpMethod         (SendMethod : "GET" : IRW)
// Selects the HTTP method that will be used when fetching content.  Valid values are "POST"
// and "GET".
// @group contentLoading
// @visibility external
//<
httpMethod:"GET", // default would POST

//> @attr htmlFlow.contentsType       (String : null : IR)
// The default setting of 'null' or 'fragment' indicates that HTML loaded from
// +link{contentsURL} is assumed to be an HTML fragment rather than a complete page.  Set to
// "page" to load HTML as a standalone page, via an IFRAME.
// <P>
// <code>contentsType:"page"</code> should only be used for controlled HTML content, and only
// when such content cannot be delivered as an HTML fragment instead (the default).  To
// dynamically load SmartClient components, use +link{ViewLoader}, <b>never</b> this mechanism
// (click +link{group:noFrames,here} for why).
// <P>
// Loading HTML content as a fragment is less resource intensive and avoids visual artifacts
// such as translucent media becoming opaque or disappearing when placed over an IFRAME.
// <P>
// Loading third-party, uncontrolled content could lead to the surrounding page disappearing if
// a user clicks on an HTML link with <code>target=_top</code>.
// <P>
// With <code>contentsType:"page"</code>, +link{loadingMessage} is not supported, and only
// "GET" is supported for +link{httpMethod,httpMethod}.
// <P>
// Note that a native bug has been observed in Internet Explorer version 10 whereby
// if an HTMLFlow with <code>contentsType</code> set to <code>"page"</code>
// loads a page containing an HTML <code>&lt;frameset&gt;</code>, when the HTMLFlow
// is +link{canvas.hide(),hidden}, it can interfere with the rendering of other elements on
// the page. Setting +link{canvas.shrinkElementOnHide} to <code>true</code> will work
// around this behavior.
//
// @group contentLoading
// @visibility external
//<

useSimpleHttp:true, // don't send stuff that RPC layer usually sends (serialized transaction, etc)

//> @attr htmlFlow.allowCaching (Boolean : false : IR)
// By default an HTMLFlow will explicitly prevent browser caching.
// <P>
// Set to true to allow browser caching <b>if the browser would normally do so</b>, in other
// words, if the HTTP headers returned with the response indicate that the response can be
// cached.
//
// @visibility external
//<

// custom properties for RPC.  Maybe be needed in the future for cache control, etc
//contentRPCProperties: null,

//> @attr htmlFlow.evalScriptBlocks (boolean : null : IR)
// If <code>evalScriptBlocks</code> is true, HTMLFlow will pre-process the loaded HTML in order to
// mimic how the HTML would execute if it were loaded as an independent page or loaded via an
// IFRAME.
// <P>
// This feature is intended to assist with migrating existing applications to SmartClient.
// <P>
// <code>evalScriptBlocks</code> is enabled by default when loading remote content (via
// +link{contentsURL}) and disabled by default for content supplied via +link{setContents()}.
// <P>
// Note that, if evalScriptBlocks is false, &lt;SCRIPT&gt; blocks will still be detected and disabled
// to avoid the inconsistent results across different browsers.
// <P>
// Only applies when contentsType is <b>not</b> "page".
//
// @group contentLoading
// @visibility external
//<
evalScriptBlocks: null,

//> @attr htmlFlow.captureSCComponents (Boolean : true : IR)
// If true, SmartClient components created while executing the loaded HTML are captured
// for rendering inside the HTMLFlow.
// <P>
// Only applies when contentsType is <b>not</b> "page".
//
// @group contentLoading
// @visibility external
//<
captureSCComponents: true
});

isc.HTMLFlow.addMethods({

initWidget : function () {
    // We can't auto-size to the contents of the IFRAME at present. When we contain an
    // IFRAME, the only meaningful overflows are "auto" or "hidden" because "visible" produces
    // inconsistent results that you would never want (never overflows in FF, oveflows to 200px
    // in IE).
    //
    // overflow: "auto" correctly introduces a native scrollbar on the IFRAME when its content
    // exceeds the space allocated to this component, but this component overflows to the max
    // space allowed by its container - which is generally what the user wants.
    //
    // ovefflow: "hidden" works like "auto", but native IFRAME scrollbars are suppressed.  This
    // is controlled in Canvas.getIFrameHTML by setting the "scrolling" property of the IFRAME.
    if (this.contentsType == "page" && this.overflow == "visible") this.setOverflow("auto");
},

// Don't load content until draw to allow declarative delayed loading
draw : function () {
    if (!this.readyToDraw()) return this;
    this.Super("draw", arguments);

    // in this case content isn't loaded until draw, when the IFRAME is created.
    // NOTE: actual code for this resides in Canvas
    var undef;
    if (this.containsIFrame()) return this;
    else if (this.canSelectText === undef) this.canSelectText = true;

    // this will cause contents to be loaded if they are not already loading
    if (this.contentsURL &&
        !(this._loadedContentsURL == this.contentsURL || this.loadingContent()))
    {
        this.setContentsURL();
    }
    return this;
},

//> @method htmlFlow.setContentsURL()
// Change the URL this component loads content from.  Triggers a fetch for content from the new
// URL.
// <p>
// Can also be called with no arguments to reload content from the existing +link{contentsURL}.
// <P>
// This feature relies on the XMLHttpRequest object which can be disabled by end-users in some
// supported browsers.  See +link{group:platformDependencies} for more information.
//
// @param [url]    (URL)      URL to retrieve contents from
// @param [params] (Object)   Parameters to send to the contentsURL.  Merged with
//                            <code>component.contentsURLParams</code> if both are set.
// @group contentLoading
// @visibility external
// @see htmlFlow.evalScriptBlocks
// @example loadHtmlPages
//<
setContentsURL : function (url, params, rpcProperties) {

    // for IFRAME-based loading, leave it up to Canvas code
    if (this.contentsType == "page") {
        return this.invokeSuper(isc.HTMLFlow, "setContentsURL", url, params);
    }

    // store new URL
    if (url != null) this.contentsURL = url;

    // during the reload, re-show the loading message
    if (this.loadingMessage) {
        var processedLoadingMsg = this.loadingMessage == null ? "&nbsp;"
                                    : this.loadingMessage.evalDynamicString(this, {
            loadingImage: this.imgHTML(isc.Canvas.loadingImageSrc,
                                       isc.Canvas.loadingImageSize,
                                       isc.Canvas.loadingImageSize)});
        this._setContents(processedLoadingMsg);
    }

    var allParams = isc.addProperties({}, this.contentsURLParams, params),
        useSimpleHttp = this.useSimpleHttp,
        httpMethod = this.httpMethod,
        serverOutputAsString = true;

    

    var reloadRequest = isc.addProperties({
        showPrompt:false,
        actionURL: this.contentsURL,
        httpMethod: httpMethod,
        useSimpleHttp: useSimpleHttp,
        // IE caches very aggressively by default, which can be confusing, so disable caching
        // unless this (currently undocumented) flag is set
        bypassCache: !this.allowCaching,
        params: allParams
    },
    this.contentRPCProperties, // widget rpc properties
    rpcProperties, // method rpc properties
    // non-overrideable
    {
        willHandleError: true,
        serverOutputAsString: serverOutputAsString,
        callback: this.getID()+"._loadContentReply(rpcRequest, rpcResponse)"
    });

    // remember the transactionNum so we load the last request only
    this._loadNumber = isc.rpc.sendProxied(reloadRequest, true).transactionNum;

},

//> @method htmlFlow.loadingContent() (A)
// Returns true if this htmlFlow is currently loading content from the server.<br>
// Note: Does not apply to htmlFlows with +link{htmlFlow.contentsType, contentsType} set to
// <code>"page"</code>
//
// @return (Boolean) whether content is currently being loaded
//
// @group contentLoading
// @see htmlFlow.contentLoaded()
// @visibility external
//<
loadingContent : function () { return this._loadNumber != null; },

_loadContentReply : function (rpcRequest, rpcResponse) {

    //!OBFUSCATEOK
    var html = rpcResponse.data;

    if (rpcResponse.status != isc.RPCResponse.STATUS_SUCCESS) {
        if (this.handleError(rpcRequest, rpcResponse) === false) return;
    }

    // handles case of setContentsURL() being called while we're fetching from some other URL
    if (rpcRequest.transactionNum != this._loadNumber) {
        return;
    }

    this._evalContents(html, (this.evalScriptBlocks == null?true:this.evalScriptBlocks));
},

//> @method htmlFlow.setContents()
// Changes the contents of a widget to newContents, an HTML string.
//
//  @param  [newContents]   (string)    an HTML string to be set as the contents of this widget
//  @visibility external
//  @see htmlFlow.evalScriptBlocks
//  @example setContents
//<
// Override the "setContents" method for enable evaluate contents
setContents : function (newContents) {
    if (newContents != null) this.contents = newContents;
    this._evalContents(this.contents, (this.evalScriptBlocks == null?false:this.evalScriptBlocks));
},


_setContents : function (newContents) {
    // invoke the super "setContents" method for internal use
    this.Super("setContents", arguments);
},

_evalContents : function (html, evalScriptBlocks) {
    // bail if there's nothing to eval
    if (!html) return;

    // We need to execute Script embedded in the HTML [which we may have to load asynchronously from
    // script src=... tags].
    // Steps:
    // 1: Extract any script tags from the HTML and store them

    // 2: Set our contents to match the sanitized HTML
    // 3: Execute scripts
    isc.HTMLFlow.getScript(html, {target:this, methodName:"_setContentsAndExecute"},
                           true, !evalScriptBlocks);
},

_relPosID: 0,
_captureSCComponentsRelPos : function (component) {
    if (!component.parentElement) this.addChild(component);

    var componentID = "HTMLFlow" + this._relPosID++;
    component.htmlElement = componentID;
    var returnHtml = '<DIV id="' + componentID + '"></DIV>';
    return returnHtml;
},

_captureSCComponentsAbsPos : function (component) {
    if (!component.parentElement) this.addChild(component);
    return null;
},

// setContentsAndExecute - helper method for 'loadContentReply()' when we have
// separated any JS from the HTML
_setContentsAndExecute : function (htmlFragments, dontFetchScripts, scripts) {
    // render the html

    this._setContents(this.transformHTML(htmlFragments.join("")));

    // Any split of htmlFragments into multiple entries must imply that a script was
    // found that must be executed.
    if (htmlFragments.length > 1) {
        if (!(dontFetchScripts)) {
            // At this point we know we don't have any script src=... blocks
            // If we have any inline script, eval it now.
            if (this.isDirty()) this.redraw();

            if (this.captureSCComponents) {
                this._oldAutoDraw = isc.Canvas.autoDraw;
                isc.setAutoDraw(false);
            }

            for (var i=0; i<htmlFragments.length; i++) {
                // put together a callback that writes directly into htmlFragments.
                // (but if !captureSCComponents, don't.)
                var callback = null;
                var mythis = this;
                if (this.captureSCComponents) callback = function (globals, error) {
                    if (!globals.length) return;
                    htmlFragments[i] = globals.map(function (newGlobalID) {
                        var newGlobal = window[newGlobalID];
                        if (!newGlobal || !isc.isA.Canvas(newGlobal)) return null;
                        if (newGlobal.position == isc.Canvas.RELATIVE)
                                return mythis._captureSCComponentsRelPos(newGlobal);
                        else    return mythis._captureSCComponentsAbsPos(newGlobal);
                    }).join("");
                };

                // note that this relies on the callback being executed
                // synchronously
                if (scripts[i]) isc.Class.globalEvalWithCapture(scripts[i], callback);
            }

            if (this.captureSCComponents) {
                this._setContents(this.transformHTML(htmlFragments.join("")));
                if (this._oldAutoDraw) {
                    // Reenable autodraw and redraw everything that originally
                    // had autodraw enabled
                    isc.setAutoDraw(true);
                    for (var global in window)
                        if (isc.isA.Canvas(global) && global.autoDraw)
                            global.markForRedraw();
                }
            }
        } //>DEBUG
        else {
            this.logWarn("html returned by server appears to contain <script> blocks.  " +
                "If you want these to be evaluated, you must set evalScriptBlocks:true.");
        }
        //<DEBUG
    }
    this._loadContentsReplyComplete();

},


//> @method htmlFlow.handleError()
//
// This method is called when a transport error occurs.  Typically, this is the result of the
// server returning an HTTP error code such as 404 - document not found.  You can inspect the
// RPCResponse object for the reasons for the error and take appropriate action.  Typical
// properties to look at are rpcResponse.status, and rpcResponse.httpResponseCode.
// <p>
// This method is called from the response processing pipeline.  If you want to provide your
// own HTML response that should be rendered into this component as the result of the error,
// you can do so by setting rpcResponse.data to your HTML string.  Returning false from this
// method suppresses any further response handling.  The default implementation of this method
// causes an error message to be logged to the Developer Console and sets the HTML to the error
// string.
//
// @param rpcRequest    (RPCRequest)      The RPCRequest that was made to the server
// @param rpcResponse   (RPCResponse)     The RPCResponse that was received
//
// @return (boolean) false to suppress further response processing
//
// @group contentLoading
// @visibility external
//<
handleError : function (rpcRequest, rpcResponse) {
    this.logWarn(rpcResponse.data);
},

_loadContentsReplyComplete : function () {
    this._loadedContentsURL = this.contentsURL;
    this._loadNumber = null;
    this.contentLoaded();
},

//> @method htmlFlow.transformHTML()
// Override to modify the loaded HTML before it is rendered.
//
// @param html (HTML) the html as loaded from the server
// return (HTML) html to be rendered
// @group contentLoading
// @visibility external
//<
transformHTML : function (html) {
    return html;
},

//> @method htmlFlow.contentLoaded()
// StringMethod fired when content is completely loaded in this htmlFlow. Has no default
// implementation. May be observed or overridden as a notification type method to fire custom
// logic when loading completes.
// <P>
// Notes:
// <ul><li>A call to +link{canvas.setContents(),this.setContents()}
//  will cause this notification to be fired when
//  the contents have been set. If +link{evalScriptBlocks} is true, and the HTML passed
//  into <code>setContents()</code> contains any <code>&lt;script src=... &gt;</code>
//  tags, this callback will be fired asynchronously once the scripts have been loaded
//  from the server and executed, as well as having the widget content updated</li>
// <li>When using +link{contentsURL}, this does not apply to htmlFlows with
//  +link{htmlFlow.contentsType, contentsType} set to <code>"page"</code></li></ul>
//
// @group contentLoading
// @visibility external
//<
contentLoaded : function () { },

modifyContent : function () { }

});

isc.HTMLFlow.registerStringMethods({

    // contentLoaded takes no arguments
    contentLoaded:""

})

//> @class HTMLPane
// Use the HTMLPane component to display HTML content in a pane of specified size. If the HTML
// content is larger than the size of the pane, the pane will provide scrollbars for viewing
// clipped content.
// <P>
// You can set the size of an HTMLPane directly via the width and height properties, or
// indirectly by placing the HTMLPane in a container component (+link{Layout}, +link{Window},
// +link{SectionStack}, etc) that manages the sizes of its members.
//
// @treeLocation Client Reference/Foundation
// @visibility external
// @example htmlPane
//<

isc.defineClass("HTMLPane", isc.HTMLFlow).addProperties({
overflow:isc.Canvas.AUTO,
defaultHeight:200
});

isc.defineClass("WSDataSource", "DataSource");

//> @class WSDataSource
// A WSDataSource is a DataSource that is preconfigured to contact the WSDL web service built
// into the SDK (see isomorphic/system/schema/SmartClientOperations.wsdl).  This WSDL service
// can be easily implemented on Java and non-Java backends.
// <P>
// WSDataSource supports all 4 DataSource operations (fetch, add, update, remove) and can be
// used with ListGrids, DynamicForms and other +link{DataBoundComponent}s just like other
// DataSources.
// <P>
// Note that WSDataSource is specifically designed for use with SmartClientOperations.wsdl.  If
// you are trying to connect to a pre-existing WSDL service, start with just +link{DataSource},
// not WSDataSource, and see the +link{group:wsdlBinding,WSDL Integration} chapter for an
// overview.
//
// @treeLocation Client Reference/Data Binding
// @visibility xmlBinding
//<

isc.WSDataSource.addMethods({
    serviceNamespace : "urn:operations.smartclient.com",
    operationBindings:[
       {operationType:"fetch", wsOperation:"fetch", recordXPath:"//data/*" },
       {operationType:"add", wsOperation:"add", recordXPath:"//data/*" },
       {operationType:"remove", wsOperation:"remove", recordXPath:"//data/*" },
       {operationType:"update", wsOperation:"update" , recordXPath:"//data/*" }
    ],
    transformRequest : function (dsRequest) {
        var data = {
            dataSource : dsRequest.dataSource,
            operationType : dsRequest.operationType,
            data : dsRequest.data
        };
        // send various metadata only if set
        if (dsRequest.startRow != null) {
            data.startRow = dsRequest.startRow;
            data.endRow = dsRequest.endRow;
        }
        if (dsRequest.textMatchStyle != null) data.textMatchStyle = dsRequest.textMatchStyle;
        if (dsRequest.operationId != null) data.operationId = dsRequest.operationId;
        if (dsRequest.sortBy != null) data.sortBy = dsRequest.sortBy;
        return data;
    },
    transformResponse : function (dsResponse, dsRequest, xmlData) {

        // Bail out early if xmlData is null or an unexpected type
        if (!xmlData || !xmlData.selectString) return;

        dsResponse.status = xmlData.selectString("//status");

        // convert status from a String to a numeric code
        if (isc.isA.String(dsResponse.status)) {
            var status = isc.DSResponse[dsResponse.status];
            if (dsResponse.status == null) {
                this.logWarn("Unable to map response code: " + status
                              + " to a DSResponse code, setting status to DSResponse.STATUS_FAILURE.");
                status = isc.DSResponse.STATUS_FAILURE;
                dsResponse.data = xmlData.selectString("//data");
            } else {
                dsResponse.status = status;
            }
        }

        // if the status is a validation error, conver the errors from XML
        if (dsResponse.status == isc.DSResponse.STATUS_VALIDATION_ERROR) {
            var errors = xmlData.selectNodes("//errors/*");
            dsResponse.errors = isc.xml.toJS(errors, null, this);
        }

        dsResponse.totalRows = xmlData.selectNumber("//totalRows");
        dsResponse.startRow = xmlData.selectNumber("//startRow");
        dsResponse.endRow = xmlData.selectNumber("//endRow");

    }
});


/*
 * Copyright (c) 2001 and beyond, Isomorphic Software
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Isomorphic Software nor the names of its
 *       contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// Visit http://www.smartclient.com for more information on Isomorphic SmartClient

//> @class RestDataSource
// The RestDataSource implements the 4 core DataSource operations using a simple protocol of
// XML or JSON requests and responses sent over HTTP, which can be easily fulfilled by any HTTP
// server technology.
// <P>
// RestDataSource is named for the
// +externalLink{http://www.google.com/search?hl=en&q=REST+HTTP,REST} (REpresentational State
// Transfer) pattern, which in brief says that simple messages passed over HTTP is a sufficient
// protocol for many web applications, without the need for further protocols such as WSDL or
// SOAP.
// <P>
// A RestDataSource is used just like a normal DataSource.  RestDataSources are pre-configured,
// using the general-purpose databinding facilities of DataSources, to expect a particular
// format for responses and to send requests in a specific format.   These request and
// response formats represent Isomorphic's recommended best practices for binding SmartClient
// to backends which do not already support a similar, pre-existing request and response
// format and where the SmartClient Java Server cannot be used.
// <P>
// If you have a pre-existing REST or WSDL service which is difficult to change, consider
// adapting SmartClient to the existing service instead, by starting with a normal
// +link{DataSource} and using the
// +link{group:clientDataIntegration,client-side data integration} facilities to create a
// mapping between SmartClient's +link{DSRequest} and +link{DSResponse} objects and the message
// formats of your existing services.
// <P>
// RestDataSource is typically used with PHP, Ruby, Python, Perl or custom server technologies,
// and represents an alternative to installing the SmartClient Server in a Java technology
// stack, or using +link{group:wsdlBinding,WSDL-based binding} with .NET or other WSDL-capable
// technologies.  Note that SmartClient Server also provides built-in support for the REST
// protocol via its RESTHandler servlet; this is primarily to allow non-SmartClient clients
// to make use of DataSource operations.  If you particularly wished to do so, you could use
// RestDataSource to make a SmartClient app talk to the SmartClient Server using REST rather
// than the proprietary wire format normally used when communicating with SmartClient Server
// (this is how we are able to write automated tests for the RESTHandler servlet).  However,
// doing this provides no benefit, imposes a number of inconveniences, and makes a handful
// of server-based features less useful
// (+link{DataSourceField.viewRequiresAuthentication,field-level declarative security}, for
// example), so we strongly recommend that you do <em>not</em> do this; it is only mentioned
// here for completeness while we are discussing REST.
// <P>
// The request and response formats used by the RestDataSource allow for many of the available
// features of SmartClient's databinding system to be used, including data paging, searching &
// sorting, +link{dsRequest.oldValues,long transactions},
// +link{ResultSet,automatic cache sync}, +link{group:relogin,relogin} and
// +link{RPCManager.startQueue,queuing}.  However,  advanced
// features such as +link{group:upload,uploading / binary fields} and
// +link{listGrid.exportData,export} aren't available with RestDataSource and need to be
// re-implemented as needed.  Most, though not all, +link{group:iscServer,server-based features}
// are still available when using RestDataSource, as long as you are also using the RESTHandler
// servlet that is part of SmartClient Server.  However, as noted above, this approach is not
// recommended; if you are using Isomorphic technology both client- and server-side, it makes
// more sense to use the proprietary wire format.
// <P>
// <b>RestDataSource and binary data</b>
// <P>
// Binary data in a response provided to a RestDataSource must be delivered as valid XML or
// JSON Strings.  Once delivered to the browser as Strings, there is no way to trigger the
// browser's "Save As" dialog to download the data, and in most cases no way to trigger other
// helper applications that might be launched to handle binary data (such as Excel or a PDF
// viewer).  Hence for binary it usually makes sense to make a direct request via
// RPCManager.sendRequest() with downloadResult:true, separate from RestDataSource.
// <P>
// If you are using the SmartClient Server included in Pro, Power end Enterprise to handle your
// REST requests server-side, there is transparent support for conversion between Java
// <code>InputStream</code>s representing binary data, and Strings containing that binary
// data encoded using the <a href=http://en.wikipedia.org/wiki/Base64>Base64 algorithm</a>.
// Thus, on the server, the binary data is in its raw binary form, with transparent conversion
// to or from Base64 for messages to or from the REST client.
// <P>
// <span style="font-weight:bold;font-size:16px;">Examples</span>
// <p>
// <b>XML formatted responses:</b>
// <P>
// RestDataSource expects a response like the following in response to a "fetch" request:
// <pre>
// &lt;response&gt;
//    &lt;status&gt;0&lt;/status&gt;
//    &lt;startRow&gt;0&lt;/startRow&gt;
//    &lt;endRow&gt;76&lt;/endRow&gt;
//    &lt;totalRows&gt;546&lt;/totalRows&gt;
//    &lt;data&gt;
//      &lt;record&gt;
//          &lt;field1&gt;value&lt;/field1&gt;
//          &lt;field2&gt;value&lt;/field2&gt;
//      &lt;/record&gt;
//      &lt;record&gt;
//          &lt;field1&gt;value&lt;/field1&gt;
//          &lt;field2&gt;value&lt;/field2&gt;
//      &lt;/record&gt;
//      <i>... 76 total records ... </i>
//    &lt;/data&gt;
// &lt;/response&gt;
// </pre>
// The &lt;status&gt; element indicates whether the fetch operation was successful
// (see +link{group:statusCodes}).
// <P>
// The &lt;data&gt; element contains a list of record nodes, each of which represents a record
// returned by the server.  The optional &lt;startRow&gt;, &lt;endRow&gt; and &lt;totalRows&gt;
// elements are needed only if data paging is in use, and populate the
// +link{dsResponse.startRow,startRow}, +link{dsResponse.endRow,endRow} and
// +link{dsResponse.totalRows,totalRows} properties of the +link{DSResponse}.
// <P>
// Note: for a more compact format, simple field values may be specified on record
// nodes directly as attributes - in this case a record element might be structured like this:
// <pre>
//     &lt;record field1="value" field2="value" /&gt;
// </pre>
// <p>
// Note that a RestDataSource will bypass browser caching of all responses by default.  See
// +link{dataSource.preventHTTPCaching}.
// <p>
// Successful "add" or "update" request responses are similar in format - in this case the data
// element would be expected to contain a single record object containing the details of the
// record, as saved on the server.
// <P>
// The response from a "remove" operation would again include status and data elements, but in
// this case, only the primary key field value(s) of the removed record would be expected to be
// present under the data element.
// <p>
// If a validation failure occurred on the server, the response would
// have status set to +link{RPCResponse.STATUS_VALIDATION_ERROR} [<code>-4</code>],
// and any validation errors could be included as per-field sub-elements of an "errors"
// element.  For a validation error, the response is not expected to contain any
// &lt;data&gt; element.
// <P>
// A response showing a validation error might look like this:
// <pre>
// &lt;response&gt;
//    &lt;status&gt;-4&lt;/status&gt;
//    &lt;errors&gt;
//      &lt;field1&gt;
//          &lt;errorMessage&gt;A validation error occurred for this field&lt;/errorMessage&gt;
//      &lt;/field1&gt;
//    &lt;/errors&gt;
// &lt;/response&gt;
// </pre>
// <P>
// An unrecoverable error, such as an unexpected server failure, can be flagged by setting
// &lt;status&gt; to -1 and setting &lt;data&gt; to an error message.  In this case the
// &lt;errors&gt; element is not used (it's specific to validation errors).  An unrecoverable
// error causes all response processing to be skipped and +link{RPCManager.handleError} to be
// invoked, which by default will show the provided error message as an alert using
// +link{classMethod:isc.warn()}.
// <p>
// <b>JSON formatted responses:</b>
// <P>
// JSON format responses are expected to contain the same data / meta-data as XMLresponses,
// encapsulated in a simple object with a <code>"response"</code> attribute.<br>
// The response to a "fetch" request would therefore have this format:<br>
// <pre>
// {
//    "response": {
//       "status": 0,
//       "startRow": 0,
//       "endRow": 76,
//       "totalRows": 546,
//       "data": [
//           {"field1": "value", "field2": "value"},
//           {"field1": "value", "field2": "value"},
//           <i>... 76 total records ...</i>
//       ]
//    }
// }
// </pre>
// The structure successful for "add", "update" and "remove" responses would be similar, though
// the data array would be expected to contain only a single object, representing the values as
// saved.  This allows the server to return values such as an auto-generated sequence
// primaryKey, a last modified timestamp, or similar server-generated field values.
// <P>
// For a remove, only the value for the primaryKey field[s] would be required.
// <p>
// For a validation error, the <code>status</code> attribute would be set to
// +link{RPCResponse.STATUS_VALIDATION_ERROR} [<code>-4</code>], and errors would
// be specified in the <code>errors</code> attribute of the response. For example:
// <pre>
// {    "response":
//      {   "status": -4,
//          "errors":
//              {   "field1": {"errorMessage": "A validation error on field1"},
//                  "field2": {"errorMessage": "A validation error on field2"}
//              }
//      }
// }
// </pre>
// An array of errors may also be returned for a single field, like this:
// <pre>
// {    "response":
//      {   "status": -4,
//          "errors":
//              {   "field1": [
//                      {"errorMessage": "First error on field1"},
//                      {"errorMessage": "Second error on field1"}
//                  ]
//              }
//      }
// }
// </pre>
// <P>
// As with the XML format above, an unrecoverable error is indicated by setting the
// <code>status</code> attribute to -1 and the <code>data</code> property to the error message.
// <P>
// <b>Server inbound data formats</b>
// <P>
// The format of data sent to the server is determined by the +link{OperationBinding.dataProtocol}
// specified for the operation. Request data is sent as parameters if the format is
// specified as <code>"getParams"</code> or <code>"postParams"</code>.
// <P>
// In this case, the parameters sent to the server will consist of the DSRequest's data, and any
// parameters explicitly specified on the DSRequest object (as +link{RPCRequest.params}.<br>
// If +link{RestDataSource.sendMetaData} is true, the DSRequest meta
// data properties will also be present as parameters, prefixed with
// +link{RestDataSource.metaDataPrefix}.
// <P>
// Example URL constructed with the metaDataPrefix set to <code>"_"</code> (the default):
// <p>
// <code>
// &nbsp;&nbsp;&nbsp;<i>[dataURL]</i>?field1=value1&_operationType=fetch&_startRow=0&_endRow=50&_sortBy=-field2&_dataSource=dsName
// </code>
// <p>
// In this case the server would be able to separate the request's data from the meta data
// via the <code>"_"</code> prefix.
// <P>
// If data is sent to the server via the <code>"postMessage"</code> dataProtocol, the data will
// be serialized as an XML or JSON message according to the <code>dataFormat</code> setting.
// Both XML and JSON messages will contain request metadata such as startRow and endRow, and
// will appear exactly as though the subset of the +link{DSRequest} that is meaningful to the
// server had been passed to +link{dataSource.xmlSerialize()} or +link{JSON.encode()}
// respectively.
// <P>
// An example of an XML message might look like this:
// <pre>
//    &lt;request&gt;
//        &lt;data&gt;
//            &lt;countryCode&gt;US&lt;/countryCode&gt;
//            &lt;countryName&gt;Edited Value&lt;/countryName&gt;
//            &lt;capital&gt;Edited Value&lt;/capital&gt;
//            &lt;continent&gt;Edited Value&lt;/continent&gt;
//        &lt;/data&gt;
//        &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//        &lt;operationType&gt;update&lt;/operationType&gt;
//    &lt/request&gt;
// </pre>
// An example of an XML message for a fetch operation passing simple criteria:
// <pre>
//    &lt;request&gt;
//        &lt;data&gt;
//            &lt;continent&gt;North America&lt;/continent&gt;
//        &lt;/data&gt;
//        &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//        &lt;operationType&gt;fetch&lt;/operationType&gt;
//        &lt;startRow&gt;0&lt;/startRow&gt;
//        &lt;endRow&gt;75&lt;/endRow&gt;
//        &lt;componentId&gt;worldGrid&lt;/componentId&gt;
//        &lt;textMatchStyle&gt;exact&lt;/textMatchStyle&gt;
//    &lt/request&gt;
// </pre>
// And an example of an XML message for a fetch operation passing AdvancedCriteria:
// <pre>
//    &lt;request&gt;
//        &lt;data&gt;
//            &lt;_constructor&gt;AdvancedCriteria&lt;/_constructor&gt;
//            &lt;operator&gt;or&lt;/operator&gt;
//            &lt;criteria&gt;
//                &lt;criterion&gt;
//                    &lt;fieldName&gt;continent&lt;/fieldName&gt;
//                    &lt;operator&gt;equals&lt;/operator&gt;
//                    &lt;value&gt;North America&lt;/value&gt;
//                &lt;/criterion&gt;
//                &lt;criterion&gt;
//                    &lt;operator&gt;and&lt;/operator&gt;
//                    &lt;criteria&gt;
//                        &lt;criterion&gt;
//                            &lt;fieldName&gt;continent&lt;/fieldName&gt;
//                            &lt;operator&gt;equals&lt;/operator&gt;
//                            &lt;value&gt;Europe&lt;/value&gt;
//                        &lt;/criterion&gt;
//                        &lt;criterion&gt;
//                            &lt;fieldName&gt;population&lt;/fieldName&gt;
//                            &lt;operator&gt;greaterThan&lt;/operator&gt;
//                            &lt;value&gt;50000000&lt;/value&gt;
//                        &lt;/criterion&gt;
//                    &lt;/criteria&gt;
//                &lt;/criterion&gt;
//            &lt;/criteria&gt;
//        &lt;/data&gt;
//        &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//        &lt;operationType&gt;fetch&lt;/operationType&gt;
//        &lt;startRow&gt;0&lt;/startRow&gt;
//        &lt;endRow&gt;75&lt;/endRow&gt;
//        &lt;componentId&gt;worldGrid&lt;/componentId&gt;
//    &lt/request&gt;
// </pre>
// An example of an XML message for a fetch operation when using +link{group:serverSummaries,server-side summaries}:
// <pre>
//    &lt;request&gt;
//        &lt;data&gt;&lt;/data&gt;
//        &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//        &lt;operationType&gt;fetch&lt;/operationType&gt;
//        &lt;summaryFunctions&gt;
//            &lt;pk&gt;count&lt;/pk&gt;
//        &lt;/summaryFunctions&gt;
//        &lt;groupBy&gt;member_g8&lt;/groupBy&gt;
//    &lt/request&gt;
// </pre>
// JSON messages are just the plain JSON form of the structures shown in the above XML
// examples. The advanced criteria XML example above but in JSON form:
// <pre>
// {
//     data: {
//         _constructor: "AdvancedCriteria",
//         operator: "or",
//         criteria: [
//             {
//                 fieldName: "continent",
//                 operator: "equals",
//                 value: "North America
//             },
//             {
//                 operator: "and", criteria: [
//                     {
//                         fieldName: "continent",
//                         operator: "equals",
//                         value: "Europe"
//                     },
//                     {
//                         fieldName: "population",
//                         operator: "greaterThan",
//                         value: 50000000
//                     }
//                 ]
//             }
//         ]
//     }
//     dataSource: "countryDS",
//     operationType: "fetch",
//     startRow: 0,
//     endRow: 75,
//     componentId: "worldGrid"
// }
// </pre>
// The +link{restDataSource.operationBindings,default OperationBindings} for a RestDataSource
// specify dataProtocol as "getParams" for the fetch operation, and "postParams" for update,
// add and remove operations.  Note that most webservers impose a limit on the maximum size
// of GET requests (specifically, on the size of the request URL + HTTP headers).  Using
// dataProtocol:"getParams" for "fetch" operations that involve complex AdvancedCriteria
// will result in a JSON serialization of the AdvancedCriteria in the request URL, and when
// combined with large cookies this can easily overflow the default limits on certain
// webservers (see
// +externalLink{http://stackoverflow.com/questions/686217/maximum-on-http-header-values}).
// For this reason, we recommend that you use the "postMessage" protocol whenever you are
// intending to use AdvancedCriteria with RestDataSource.

// <P>
// <b>Date, time and datetime values</b>
// <P>
// Date, time and datetime values must be communicated using XML Schema format, as in the
// following examples:
// <pre>
// &nbsp;&nbsp;&lt;dateField&gt;2007-04-22&lt;/dateField&gt;
// &nbsp;&nbsp;&lt;timeField&gt;11:07:13&lt;/timeField&gt;
// &nbsp;&nbsp;&lt;dateTimeField&gt;2007-04-22T11:07:13&lt;/dateTimeField&gt;
// &nbsp;&nbsp;&lt;dateTimeField&gt;2007-04-22T11:07:13.582&lt;/dateTimeField&gt;
// </pre>
// <P>
// And the equivalent in JSON:
// <pre>
// &nbsp;&nbsp;dateField: "2007-04-22"
// &nbsp;&nbsp;timeField: "11:07:13"
// &nbsp;&nbsp;dateTimeField: "2007-04-22T11:07:13"
// &nbsp;&nbsp;dateTimeField: "2007-04-22T11:07:13.582"
// </pre>
// <P>
// Both RestDataSource on the client-side and the RESTHandler servlet on the server side
// automatically handle encoding and decoding temporal values using these formats.  Both also
// handle datetime formats including or excluding milliseconds automatically.  When encoding,
// both honot the +link{DataSource.trimMilliseconds} setting on the DataSource, falling back
// to the <code>server.properties</code> setting <code>rest.trimMilliseconds</code>; when
// decoding, both detect whether or not to try to parse milliseconds based on the string they
// receive.
// <P>
// Fields of type "date" and "time" are considered to hold logical date and time values, as
// discussed in the +link{group:dateFormatAndStorage,date and time handling article}, and are
// not affected by timezones.  Fields of type "datetime" will be converted to UTC on the
// client side by RestDataSource, and will be sent back down to the client as UTC by the
// server-side RESTHandler.  We recommend that your own REST client and/or server code do the
// same thing (ie, transmit all datetime values in both directions as UTC).  Note that the
// examples given above give no timezone information, and will be treated by the SmartClient
// Server as UTC values.  If you wish to work with datetime values in a particular timezone,
// use a format like this:
// <pre>
// &nbsp;&nbsp;&lt;dateField&gt;2007-04-22T11:07:13-0800&lt;/dateField&gt;
// &nbsp;&nbsp;&lt;dateField&gt;2012-11-19T22:12:04+0100&lt;/dateField&gt;
// </pre>
// <p>
// And the equivalent in JSON:
// <pre>
// <code>&nbsp;&nbsp;dateTimeField: "2007-04-22T11:07:13-0800"
// <code>&nbsp;&nbsp;dateTimeField: "2012-11-19T22:12:04+0100"
// </pre>
// <P>
// <b>NOTE:</b> Although we refer above to XML Schema format, the format used for specifying
// timezone offset is slightly different from XML Schema - as shown in the above examples, you
// specify "+HHMM" or "-HHMM", as opposed to the XML Schema format which requires a ":" character
// between the hours and minutes.  The reason for this difference is simply that the Java
// SimpleDateFormat class imposes it.
// <p>
// <b>RestDataSource queuing support</b>
// <P>
// RestDataSource supports +link{RPCManager.startQueue,queuing} of DSRequests.  This allows
// you to send multiple requests to the server in a single HTTP turnaround, thus minimizing
// network traffic and allowing the server to treat multiple requests as a single transaction,
// if the server is able to do so (in Power Edition and above, the SmartClient Server
// transparently supports grouping multiple REST requests in a queue into a single database
// transaction when using one of the built-in DataSource types).  Note that you can disable
// queuing support with the +link{RestDataSource.disableQueuing} flag.
// <P>
// If you want to use queuing with RestDataSource, you must use the "postMessage" dataProtocol
// with either XML or JSON dataFormat.  Message format is similar to the non-queued examples
// shown earlier: it is simply extended to cope with the idea of multiple DSRequests
// encapsulated in the message.
// <P>
// An example of the XML message sent from RestDataSource to the server for two update requests
// combined into a queue, using XML dataFormat:
// <pre>
// &lt;transaction&gt;
//     &lt;operations&gt;
//         &lt;request&gt;
//             &lt;data&gt;
//                 &lt;pk&gt;1&lt;/pk&gt;
//                 &lt;countryName&gt;Edited Value&lt;/countryName&gt;
//                 &lt;capital&gt;Edited Value&lt;/capital&gt;
//                 &lt;continent&gt;Edited Value&lt;/continent&gt;
//             &lt;/data&gt;
//             &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//             &lt;operationType&gt;update&lt;/operationType&gt;
//         &lt/request&gt;
//         &lt;request&gt;
//             &lt;data&gt;
//                 &lt;pk&gt;2&lt;/pk&gt;
//                 &lt;capital&gt;Edited Value&lt;/capital&gt;
//                 &lt;population&gt;123456&lt;/population&gt;
//             &lt;/data&gt;
//             &lt;dataSource&gt;countryDS&lt;/dataSource&gt;
//             &lt;operationType&gt;update&lt;/operationType&gt;
//         &lt/request&gt;
//     &lt;/operations&gt;
// &lt;transaction&gt;
// </pre>
// And the same message in JSON format:
// <pre>
// {
//     transaction: {
//         operations: [{
//             dataSource:"countryDS",
//             operationType:"update",
//             data: {
//                 pk: 1
//                 countryName: "Edited Value",
//                 capital: "Edited Value",
//                 continent: "Edited Value"
//             }
//         }, {
//             dataSource:"countryDS",
//             operationType:"update",
//             data: {
//                 pk: 2,
//                 capital: "Edited Value",
//                 popuilation: 123456
//             }
//         }]
//     }
// }
// </pre>
// RestDataSource expects the response to a queue of requests to be a queue of responses in
// the same order as the original requests.  Again, the message format is very similar to the
// unqueued REST format, it just has an outer container construct.  Note also that the
// individual DSResponses in a queued response have an extra property,
// +link{DSResponse.queueStatus,<code>queueStatus</code>}.  This allows each individual
// response to determine whether the queue as a whole succeeded.  For example, if the first
// update succeeded but the second failed validation, the first response would have a
// <code>status</code> of 0, but a <code>queueStatus</code> of -1, while the second response
// would have both properties set to -1.
// <P>
// The update queue example given above would expect a response like this (in XML):
// <pre>
// &lt;responses&gt;
//     &lt;response&gt;
//         &lt;status&gt;0&lt;/status&gt;
//         &lt;queueStatus&gt;0&lt;/queueStatus&gt;
//         &lt;data&gt;
//             &lt;record&gt;
//                 &lt;countryName&gt;Edited Value&lt;/countryName&gt;
//                 &lt;gdp&gt;1700.0&lt;/gdp&gt;
//                 &lt;continent&gt;Edited Value&lt;/continent&gt;
//                 &lt;capital&gt;Edited Value&lt;/capital&gt;
//                 &lt;pk&gt;1&lt;/pk&gt;
//             &lt;/record&gt;
//         &lt;/data&gt;
//     &lt;/response&gt;
//     &lt;response&gt;
//         &lt;status&gt;0&lt;/status&gt;
//         &lt;queueStatus&gt;0&lt;/queueStatus&gt;
//         &lt;data&gt;
//             &lt;record&gt;
//                 &lt;countryName&gt;United States&lt;/countryName&gt;
//                 &lt;gdp&gt;7247700.0&lt;/gdp&gt;
//                 &lt;continent&gt;North America&lt;/continent&gt;
//                 &lt;independence&gt;1776-07-04&lt;/independence&gt;
//                 &lt;capital&gt;Washington DC&lt;/capital&gt;
//                 &lt;pk&gt;2&lt;/pk&gt;
//                 &lt;population&gt;123456&lt;/population&gt;
//             &lt;/record&gt;
//         &lt;/data&gt;
//     &lt;/response&gt;
// &lt;/responses&gt;
// </pre>
// And in JSON:
// <pre>
// [
// {
//     "response": {
//         "queueStatus": 0,
//         "status": 0,
//         "data": [{
//             "countryName": "Edited Value",
//             "gdp": 1700.0,
//             "continent": "Edited Value",
//             "capital": "Edited Value",
//             "pk": 1
//         }]
//     }
// },
// {
//     "response": {
//         "queueStatus": 0,
//         "status": 0,
//         "data": [{
//             "countryName": "United States",
//             "gdp": 7247700.0,
//             "continent": "North America",
//             "independence": "1776-07-04",
//             "capital": "Washington DC",
//             "pk": 2,
//             "population": 123456
//         }]
//     }
// }
// ]
// </pre>
// <b>Hierarchical (Tree) data:</b>
// <P>
// To create a hierarchical DataSource, in the DataSource's <code>fields</code> array, a field
// must be specified as the parent id field - the field which will contain a pointer to the
// id of each node's parent.
// This can be achieved by setting the +link{DataSourceField.foreignKey} and the
// +link{DataSourceField.rootValue} attributes on the field definition. For example:
// <pre>
// RestDataSource.create({
//    ID:"supplyItem",
//    fields : [
//        {name:"itemId", type:"sequence", primaryKey:true},
//        {name:"parentId", type:"integer", foreignKey:"supplyItem.itemId", rootValue:0},
//        ...
//    ]
// });
// </pre>
// Tree Data is then treated on the server as a flat list of records linked by parent id.
// <P>
// Tree data is typically displayed using a dataBound +link{class:TreeGrid} component. TreeGrids
// automatically create a +link{class:ResultTree} data object, which requests data directly
// from the DataSource.  ResultTrees load data on demand, only requesting currently visible
// (open) nodes from the server. This is handled by including a specified value for the parent
// id field in the request criteria.<br>
// To implement a standard load-on-demand tree RestDataSource back end, you should therefore
// simply return the set of nodes that match the criteria passed in.
// For example, if your DataSource was defined as the "supplyItem" code snippet above,
// a fetch request for all children of a node with <code>itemId</code> set to <code>12</code>
// would have <code>"parentId"</code> set to <code>12</code> in the request criteria.
// A valid response would then contain all the records that matched this criteria. For example:
// <pre>
// &lt;response&gt;
//    &lt;status&gt;0&lt;/status&gt;
//    &lt;data&gt;
//      &lt;record&gt;
//          &lt;itemId&gt;15&lt;/itemId&gt;
//          &lt;parentId&gt;12&lt;/parentId&gt;
//      &lt;/record&gt;
//      &lt;record&gt;
//          &lt;itemId&gt;16&lt;/itemId&gt;
//          &lt;parentId&gt;12&lt;/parentId&gt;
//      &lt;/record&gt;
//    &lt;/data&gt;
// &lt;/response&gt;
// </pre>
// The structure of responses for Add, Update and Delete type requests will be the
// same regardless of whether the data is hierarchical. However you should be aware that
// the underlying data storage may need to be managed slightly differently in some cases.
// <P>
// Specifically, Add and Update operations may change the structure of the tree by returning a
// new parent id field value for the modified node. Depending on how your data is stored you
// may need to include special back-end logic to handle this.
// <P>
// Also, if a user deletes a folder within a databound tree, any children of that folder will
// also be dropped from the tree, and can be removed from the back-end data storage.
// <P>
// Note: For a general overview of binding components to Tree structured data, see
// +link{group:treeDataBinding, Tree Databinding}.
//
// @treeLocation Client Reference/Data Binding
// @visibility external
// @example restEditSave
//<
isc.defineClass("RestDataSource", "DataSource");


isc.RestDataSource.addProperties({
    //> @attr restDataSource.dataProtocol (DSProtocol : null : [IR])
    // Rather than setting +link{dataSource.dataProtocol}, to control the format in which
    // inputs are sent to the dataURL, you must specify a replacement +link{OperationBinding}
    // and specify +link{OperationBinding.dataProtocol} on that <code>operationBinding</code>.
    // <P>
    // This is because <code>RestDataSource</code> specifies default
    // <code>operationBindings</code> for all operationTypes - see
    // +link{restDataSource.operationBindings}.
    //
    // @group clientDataIntegration
    // @group serverDataIntegration
    // @serverDS allowed
    // @visibility external
    //<

    serverType:"generic",

    //> @attr restDataSource.dataFormat (DSDataFormat : "xml" : IR)
    // Expected format for server responses. RestDataSources handle <code>"json"</code> and
    // <code>"xml"</code> format responses by default. See class overview documentation for
    // examples of responses in each format.
    // @visibility external
    //<
    dataFormat:"xml",

    //> @attr restDataSource.xmlRecordXPath    (string : "/response/data/*" : IR)
    // <code>recordXPath</code> mapping to the data node of XML returned by the server.
    // Applies if this.dataFormat is set to <code>"xml"</code>.<br>
    // The default value will pick up data from a response structured as follows:<br>
    // <pre>
    // &lt;response&gt;
    //    &lt;status&gt;0&lt;/status&gt;
    //    &lt;data&gt;
    //      &lt;record&gt;
    //          &lt;field1&gt;value&lt;/field1&gt;
    //          &lt;field2&gt;value&lt;/field2&gt;
    //      &lt;/record&gt;
    //      &lt;record&gt;
    //          &lt;field1&gt;value&lt;/field1&gt;
    //          &lt;field2&gt;value&lt;/field2&gt;
    //      &lt;/record&gt;
    //    &lt;/data&gt;
    // &lt;/response&gt;
    // </pre>
    // @visibility external
    //<
    xmlRecordXPath:"/response/data/*",

    //> @attr restDataSource.xmlNamespaces (Object : See below : IR)
    // When +link{dataFormat} is "xml", <code>xmlNamespaces</code> configures the set of
    // namespace prefixes that are added to the document element of the XML message sent to the
    // server.  Format is the same as +link{dataSource.xmlNamespaces}.
    // <P>
    // By default, the "xsi" prefix is bound to "http://www.w3.org/2001/XMLSchema-instance" in
    // order to allow explicit null values in Records to be sent for
    // +link{dataSourceField.nillable,fields declared nillable}.  Set to null to avoid any
    // prefixes being added.
    //
    // @see dataSourceField.nillable
    // @visibility external
    //<
    xmlNamespaces : { xsi: "http://www.w3.org/2001/XMLSchema-instance" },

    //> @attr restDataSource.jsonRecordXPath    (string : "/response/data" : IR)
    // <code>recordXPath</code> mapping to the data node of json returned by the server.
    // Applies if this.dataFormat is set to <code>"json"</code><br>
    // The default value will pick up data from a response structured as follows:<br>
    // <pre>
    // {response:
    //  {status:0,
    //   data:[
    //      {field1:"value", field2:"value"},
    //      {field1:"value", field2:"value"}
    //   ]
    // }
    // </pre>
    // @visibility external
    //<
    jsonRecordXPath:"/response/data",

    //> @attr restDataSource.recordXPath (string : null : IRW)
    // For RestDataSources, by default, either the +link{RestDataSource.xmlRecordXPath} or
    // +link{RestDataSource.jsonRecordXPath} is used based on the +link{dataFormat}
    // setting.
    // <P>
    // Note that you can also apply record xpath binding via
    // +link{operationBinding.recordXPath}.
    //
    // @visibility external
    //<

    //> @attr restDataSource.prettyPrintJSON (Boolean : true : IR)
    // When using dataFormat:"json" and dataProtocol:"postMessage" should we use the
    // +link{JSONEncoder.prettyPrint} feature to enable indented, highly readable JSON messages.
    // <P>
    // True by default because the bandwidth involved is generally negligible and the benefits for
    // troubleshooting are key.
    //
    // @visibility external
    //<
    prettyPrintJSON: true,

    dataFormatParamName: "isc_dataFormat",

    //> @attr restDataSource.disableQueuing (Boolean : false : IRW)
    // If set, disables +link{RPCManager.startQueue,request queuing} for this RestDataSource.
    //
    // @visibility external
    //<


    //> @attr restDataSource.jsonPrefix (String : See below : IRW)
    // Allows you to specify an arbitrary prefix string to apply to all json format responses
    // sent from the server to this application.  The client will expect to find this prefix
    // on any JSON response received for this DataSource, and will strip it off before evaluating
    // the response text.
    // <p>
    // The default prefix is "&lt;SCRIPT&gt;//'\"]]&gt;&gt;isc_JSONResponseStart&gt;&gt;".
    // <p>
    // The inclusion of such a prefix ensures your code is not directly executable outside of
    // your application, as a preventative measure against
    // <a href='http://www.google.com/search?q=javascript+hijacking'>javascript hijacking</a>.
    // <p>
    // You can switch off JSON wrapping altogether by setting both this and +link{jsonSuffix}
    // to empty strings.
    // <p>
    // If you are using SmartClient Server's RESTHandler servlet, see the server-side Javadocs
    // for details of how to change the way JSON wrapping works on the server side.
    //
    // @see RestDataSource.jsonSuffix
    // @visibility external
    //<
    jsonPrefix: "<SCRIPT>//'\"]]>>isc_JSONResponseStart>>",

    //> @attr restDataSource.jsonSuffix (String : See below : IRW)
    // Allows you to specify an arbitrary suffix string to apply to all json format responses
    // sent from the server to this application.  The client will expect to find this suffix
    // on any JSON response received for this DataSource, and will strip it off before evaluating
    // the response text.
    // <p>
    // The default suffix is "//isc_JSONResponseEnd".
    // @see RestDataSource.jsonPrefix
    // @visibility external
    //<
    jsonSuffix: "//isc_JSONResponseEnd",

    // Override init to pick up these paths
    init : function () {

        this.serverType = "generic";
        this.recordXPath = this.recordXPath ||
                (this.dataFormat == "xml" ? this.xmlRecordXPath : this.jsonRecordXPath);
        return this.Super("init", arguments);
    },

    //Added to resolve an issue with recursion within SmartGWT, when the
    // fetchDataURL property is being requested.
    getProperty : function (propName) {
        if (propName == "fetchDataURL") return this.fetchDataURL;
        var getter = this._getGetter(propName);
        if (getter) return this[getter]();
        return this[propName];
    },

    //> @attr RestDataSource.operationBindings (Array of OperationBinding : [...] : IR)
    // RestDataSource OperationBindings set to specify default dataProtocol per operationType.
    // Default databindings are:
    // <pre>
    //   operationBindings : [
    //     {operationType:"fetch", dataProtocol:"getParams"},
    //     {operationType:"add", dataProtocol:"postParams"},
    //     {operationType:"remove", dataProtocol:"postParams"},
    //     {operationType:"update", dataProtocol:"postParams"}
    //   ],
    // </pre>
    // If you are integrating with a +link{RestDataSource,REST} server that requires the more
    // obscure +link{rpcRequest.httpMethod}s of "PUT", "DELETE" or "HEAD", you can specify these
    // httpMethod settings via +link{operationBinding.requestProperties}.  dataProtocol settings
    // that mention "GET" or "POST" are compatible with these additional HTTP methods as well.
    // Typical +link{dataSource.operationBindings,operationBindings} for a REST server that uses
    // "PUT" and "DELETE" are as follows:
    // <pre>
    //   operationBindings:[
    //     {operationType:"fetch", dataProtocol:"getParams"},
    //     {operationType:"add", dataProtocol:"postParams"},
    //     {operationType:"remove", dataProtocol:"getParams", requestProperties:{httpMethod:"DELETE"}},
    //     {operationType:"update", dataProtocol:"postParams", requestProperties:{httpMethod:"PUT"}}
    //   ],
    // </pre>
    // <p>
    // Note that dataProtocol:"postMessage" is always used when
    // +link{RPCManager.startQueue,queuing} is used to send multiple DSRequests to the server
    // as a single HttpRequest.  See +link{RestDataSource} docs, "queuing support".  We also
    // recommend that you use the "postMessage" protocol whenever you are intending to use
    // AdvancedCriteria with RestDataSource - this is discussed in the section "Server inbound
    // data format" in the +link{class:RestDataSource,RestDataSource overview}.
    // <p>
    //
    // @visibility external
    //<
    operationBindings:[
       {operationType:"fetch", dataProtocol:"getParams"},
       {operationType:"add", dataProtocol:"postParams"},
       {operationType:"remove", dataProtocol:"postParams"},
       {operationType:"update", dataProtocol:"postParams"}
    ],

    //> @attr RestDataSource.dataURL  (string : null : IR)
    // Default URL to contact to fulfill all DSRequests.
    // RestDataSources also allow per-operationType dataURLs to be set via
    // <ul>
    // <li>+link{RestDataSource.fetchDataURL}</li>
    // <li>+link{RestDataSource.addDataURL}</li>
    // <li>+link{RestDataSource.updateDataURL}</li>
    // <li>+link{RestDataSource.removeDataURL}</li>
    // </ul>
    // @visibility external
    //<

    //> @attr RestDataSource.fetchDataURL    (string : null : IR)
    // Custom dataURL for fetch type operations
    // @visibility external
    //<

    //> @attr RestDataSource.updateDataURL    (string : null : IR)
    // Custom dataURL for update type operations
    // @visibility external
    //<

    //> @attr RestDataSource.addDataURL    (string : null : IR)
    // Custom dataURL for add type operations
    // @visibility external
    //<

    //> @attr RestDataSource.removeDataURL    (string : null : IR)
    // Custom dataURL for remove type operations
    // @visibility external
    //<

    //> @attr RestDataSource.sendMetaData (Boolean : true : IR)
    // Should operation meta data be included when assembling parameters to send
    // to the server? If true, meta data parameters will be prefixed with the
    // +link{RestDataSource.metaDataPrefix}.<br>
    // Applies to operations where OperationBinding.dataProtocol is set to
    // <code>"getParams"</code> or <code>"postParams"</code> only.
    // @visibility external
    //<
    sendMetaData:true,

    //> @attr RestDataSource.metaDataPrefix   (string : "_" :IR)
    // If +link{RestDataSource.sendMetaData} is true, this attribute is used to specify
    // the prefix to apply to 'meta data' properties when assembling parameters to send to the
    // server.  Applies to operations where OperationBinding.dataProtocol is set to
    // <code>"getParams"</code> or <code>"postParams"</code> only.
    // @visibility external
    //<
    metaDataPrefix:"_",

    //> @attr RestDataSource.sendClientContext (boolean : null : IRW)
    // If true the +link{dsRequest.clientContext} will be sent to the server as a parameter
    // along with the request.
    // @visibility internal
    //<
    // only has an effect for "postMessage" data protocol

    // sendClientContext:null,

    // getDataURL()
    // overridden to respect fetchDataURL et al.
    getDataURL : function (dsRequest) {
        var type = dsRequest.operationType;

        if (type == "fetch" && this.fetchDataURL != null)
            return this.fetchDataURL;
        if (type == "update" && this.updateDataURL != null)
            return this.updateDataURL;
        if (type == "add" && this.addDataURL != null)
            return this.addDataURL;
        if (type == "remove" && this.removeDataURL != null)
            return this.removeDataURL;
        return this.Super("getDataURL", arguments);
    },

    // Override getDataProtocol - treat postXML dataProtocol specification as postMessage.
    getDataProtocol : function (dsRequest) {
        var protocol = this.Super("getDataProtocol", arguments);
        if (protocol == "postXML") protocol = "postMessage";
        return protocol;
    },

    //> @method RestDataSource.transformRequest()
    // RestDataSource overrides transformRequest and handles serializing the request in the
    // appropriate format (determined by the specified
    // +link{operationBinding.dataProtocol,dataProtocol}), including the submitted
    // +link{DSRequest.data,data} as well as the meta data parameters, which may include -<br>
    // +link{DSRequest.dataSource,dataSource},
    // +link{DSRequest.operationType,operationType}, +link{DSRequest.operationId,operationId};<br>
    // +link{DSRequest.startRow,startRow} and +link{DSRequest.endRow,endRow} (for fetches);<br>
    // +link{DSRequest.sortBy,sortBy} and +link{DSRequest.textMatchStyle,textMatchStyle}
    // (for fetches);<br>
    // +link{DSRequest.oldValues,oldValues} (for update and remove operations);<br>
    // and possibly +link{DSRequest.componentId,componentId}.
    // <P>
    // If you override this method in order to add additional data to the DSRequest, you must
    // call +link{Class.Super,Super()} or you will remove the functionality provided by
    // RestDataSource.  For example:
    // <pre>
    //    transformRequest : function (dsRequest) {
    //        // modify dsRequest.data here, for example, add fixed criteria
    //        dsRequest.data.userId = myApplication.getCurrentUserId();
    //
    //        return this.Super("transformRequest", arguments);
    //    }
    // </pre>
    // <P>
    // See +link{class:RestDataSource,RestDataSource overview} for a description of the
    // standard formatting applied to requests.
    //
    // @visibility external
    //<
    transformRequest : function (dsRequest) {
        var protocol = this.getDataProtocol(dsRequest);

        dsRequest.isRestRequest = !(this.disableQueuing || this.clientOnly);

        // Lets override the dataformat for client only datasources otherwise use the DataSource preferred format.
        dsRequest.dataFormat = (this.clientOnly ? "json" : this.dataFormat);

        // "postMessage": Post data as XML serialized message
        if (protocol == "postMessage") {

            // Set parameter specifying request/response data format
            if (dsRequest.params == null) {
                dsRequest.params = {};
            }
            dsRequest.params[this.dataFormatParamName] = this.dataFormat;

            var params = {
                dataSource:this.getID()
            };

            // omit metadata fields if they're not set on the dsRequest
            if (dsRequest.operationType != null) params.operationType = dsRequest.operationType;
            if (dsRequest.operationId != null) params.operationId = dsRequest.operationId;
            if (dsRequest.startRow != null) params.startRow = dsRequest.startRow;
            if (dsRequest.endRow != null) params.endRow = dsRequest.endRow;
            if (dsRequest.sortBy != null) params.sortBy = dsRequest.sortBy;
            if (dsRequest.textMatchStyle != null) params.textMatchStyle = dsRequest.textMatchStyle;
            if (dsRequest.parentNode != null) params.parentNode = isc.Tree.getCleanNodeData(dsRequest.parentNode);
            if (dsRequest.useStrictJSON != null) params.useStrictJSON = dsRequest.useStrictJSON;

            if (this.sendClientContext) params.clientContext = dsRequest.clientContext;

            // send the componentId if present
            if (dsRequest.componentId) params.componentId = dsRequest.componentId;

            if (isc.DataSource.get("__criteriaSerializeDS") == null) {
                isc.DataSource.create({
                    ID: "__criteriaSerializeDS",
                    fields:[
                        {name:"_constructor", xmlAttribute:false},
                        {name:"criteria", multiple: true, type: "__criteriaSerializeDS", childTagName: "criterion"},
                        {name:"oldValues"}
                    ]
                });
            }

            var ds = isc.DataSource.create({
                fields:[
                    {name:"data", type: "__criteriaSerializeDS"},
                    {name:"oldValues"}
                ]
            });

            if (this.autoConvertRelativeDates == true) {
                // convert any relative dates in criteria into absolute dates so the server
                // doesn't need to know how to handle relative dates
                if (this.logIsInfoEnabled("relativeDates")) {
                    this.logInfo("Calling convertRelativeDates from getServiceInputs "+
                        "- data is\n\n"+isc.echoFull(dsRequest.data));
                }

                var transformedData = this.convertRelativeDates(dsRequest.data);

                if (this.logIsInfoEnabled("relativeDates")) {
                    this.logInfo("Called convertRelativeDates from getServiceInputs "+
                        "- data is\n\n"+isc.echoFull(transformedData));
                }
                dsRequest.data = transformedData;
            }

            // Map across the summary functions and group by data
            if (dsRequest.summaryFunctions != null) params.summaryFunctions = dsRequest.summaryFunctions;
            if (dsRequest.groupBy != null) params.groupBy = dsRequest.groupBy;

            params.data = dsRequest.data;
            params.oldValues = dsRequest.oldValues;

            if (!dsRequest.contentType) {
                dsRequest.contentType = (this.dataFormat == "json" ?
                                         "application/json" : "text/xml");
            }

            var returnVal;

            dsRequest._unserializedData = dsRequest.data;

            if (dsRequest.dataFormat == "json") {
                if (params.data != null) params.data = this.serializeFields(params.data);
                if (params.oldValues != null) params.oldValues = this.serializeFields(params.oldValues);
                var settings = {
                    prettyPrint: this.prettyPrintJSON,
                    trimMilliseconds: !!this.trimMilliseconds
                };
                returnVal = isc.JSON.encode(params, settings);
            } else {
                var flags = {
                    // Switch off the special treatment of "_constructor" in xmlSerialize
                    ignoreConstructor: true,
                    // Provide this DS as schema to drive the serialization of individual
                    // fields, so that we can use a different DS to drive the serialization
                    // as a whole (what tags get output for criteria, criterion, etc)
                    schema: this
                };
                returnVal = ds.xmlSerialize(params, flags, null, "request");
            }

            // Don't leak the temp-dataSource objects
            ds.destroy();
            return returnVal;

        // "getParams" / "postParams": HTTP Parameters format
        } else {
            if (protocol != "getParams" && protocol != "postParams") {
                this.logWarn("RestDataSource operation:"+ dsRequest.operationID + ", of type "
                             + dsRequest.operationType + " has dataProtocol specified as '"
                             + protocol + "'. Supported protocols are 'postParams', 'getParams' "
                             + "and 'postMessage' only. Defaulting to 'getParams'.");
                dsRequest.dataProtocol = 'getParams';
            }

            // All fields passed in as 'data' will be available directly as parameters
            // Also include any explicit parameters present on the dsRequest
            var params = isc.addProperties({}, dsRequest.data, dsRequest.params);

            // Map across the summary functions and group by data
            if (dsRequest.summaryFunctions != null) params.summaryFunctions = dsRequest.summaryFunctions;
            if (dsRequest.groupBy != null) params.groupBy = dsRequest.groupBy;

            // Attach meta data parameters to the transaction
            if (this.sendMetaData) {
                if (!this.parameterNameMap) {
                    var map = {};

                    map[this.metaDataPrefix + "operationType"] = "operationType";
                    map[this.metaDataPrefix + "operationId"] = "operationId";
                    map[this.metaDataPrefix + "startRow"] = "startRow";
                    map[this.metaDataPrefix + "endRow"] = "endRow";
                    map[this.metaDataPrefix + "sortBy"] = "sortBy";
                    map[this.metaDataPrefix + "useStrictJSON"] = "useStrictJSON";
                    map[this.metaDataPrefix + "textMatchStyle"] = "textMatchStyle";
                    map[this.metaDataPrefix + "oldValues"] = "oldValues";
                    map[this.metaDataPrefix + "componentId"] = "componentId";
                    map[this.metaDataPrefix + "parentNode"] = "parentNode";

                    this.parameterNameMap = map;
                }

                // Meta data will be available as parameters with the metaDataPrefix applied
                for (var parameterName in this.parameterNameMap) {
                    var value = dsRequest[this.parameterNameMap[parameterName]];
                    if (value != null) {
                        if (parameterName == "_parentNode") {
                            params[parameterName] = isc.Tree.getCleanNodeData(value);
                        } else {
                            params[parameterName] = value;
                        }
                    }
                }
                params[this.metaDataPrefix + "dataSource"] = this.getID();
                params["isc_metaDataPrefix"] = this.metaDataPrefix;
            }
            // Set parameter specifying response data format
            params[this.dataFormatParamName] = this.dataFormat;

            return params;
        }
    },

    // getUpdatedData() overridden to use request.originalData if dataProtocol is "postMessage"
    // as in this case request.data will be a serialized string of data rather than a javascript
    // object.
    getUpdatedData : function (dsRequest, dsResponse, useDataFromRequest) {
        var data = dsResponse ? dsResponse.data : null;
        if (useDataFromRequest &&
            (!data || isc.isAn.emptyString(data) ||
             (isc.isA.Array(data) && data.length == 0)) &&
            dsResponse.status == 0 &&
            this.getDataProtocol(dsRequest) == "postMessage")
        {
            this.logInfo("dsResponse for successful operation of type " + dsRequest.operationType +
                         " did not return updated record[s]. Using submitted request data to update"
                          + " ResultSet cache.", "ResultSet");
            // look at the originalData object - stored before transformRequest was called
            var updateData = {},
                requestData = dsRequest.originalData;

            if (requestData && isc.isAn.Object(requestData)) {

                // if operationType is an update, request data will be sparse so need to combine
                // with oldValues
                if (dsRequest.operationType == "update") {
                    updateData = isc.addProperties({}, dsRequest.oldValues);

                    // Assertion - we only update one record at a time, so if submitted data is an array
                    // it will contain one entry that matches the 'oldValues'
                    if (isc.isAn.Array(requestData)) {
                        updateData = isc.addProperties(updateData, requestData[0]);
                    } else {
                        updateData = isc.addProperties(updateData, requestData);
                    }
                    updateData = [updateData];
                // for add or delete old values are irrelevant
                } else {
                    if (!isc.isAn.Array(requestData)) requestData = [requestData];
                    updateData = [];
                    for (var i = 0; i < requestData.length; i++) {
                        updateData[i] = isc.addProperties({}, requestData[i]);
                    }
                }
                //>DEBUG
                if (this.logIsDebugEnabled("ResultSet")) {
                    this.logDebug("Submitted data to be integrated into the cache:"
                                  + this.echoAll(updateData), "ResultSet");
                }
                //<DEBUG
            }
            return updateData;

        } else {
            return this.Super("getUpdatedData", arguments);
        }

    },

    // Helper method to verify the status returned by the server is valid
    getValidStatus : function (status) {
        if (isc.isA.String(status)) {
            if (parseInt(status) == status) status = parseInt(status);
            else {
                status = isc.DSResponse[status];
                if (status == null) {
                    this.logWarn("Unable to map response code: " + status
                                  + " to a DSResponse code, setting status to DSResponse.STATUS_FAILURE.");
                    status = isc.DSResponse.STATUS_FAILURE;
                }
            }
        }
        if (status == null) status = isc.DSResponse.STATUS_SUCCESS;
        return status;
    },

    // Helper method to verify the value for invalidateCache returned by the server is valid.
    // As invalidateCache is not mandatory field, set value to FALSE if not exists
    getValidInvalidateCache : function (ic) {
        if (ic == null) return false;
        switch (ic.toLowerCase()) {
            case "true": case "yes": case "1": return true;
            case "false": case "no": case "0": case null: return false;
            default: return Boolean(ic);
        }
    },

    //> @method RestDataSource.transformResponse()
    // RestDataSource implements transformResponse in order to extract data and meta-data
    // properties from server responses, as described in the
    // +link{class:RestDataSource,RestDataSource overview}.
    // <P>
    // You can override <code>transformResponse()</code> in order to further modify the
    // response, but if you do so, call +link{class.Super,Super()} as shown below or you will
    // wipe out the built-in response processing behavior of RestDataSource.
    // <pre>
    // transformResponse : function (dsResponse, dsRequest, data) {
    //     var dsResponse = this.Super("transformResponse", arguments);
    //     // ... do something to dsResponse ...
    //     return dsResponse;
    // }
    // </pre>
    // @param dsResponse (DSResponse) default DSResponse derived from the response data
    // @param dsRequest (DSRequest) DSRequest object that initiated this request
    // @param data (XMLDocument or JSON) XML document or JSON objects returned by the web
    //                                   service
    // @return (DSResponse) response derived
    //
    // @visibility external
    //<
    transformResponse : function (dsResponse, dsRequest, data) {
        if (dsResponse.status < 0 || !data) {
            // If any request failed, queueStatus must be -1
            dsResponse.queueStatus = -1;
            return dsResponse;
        }

        if (dsRequest.dataFormat == "json") {
            if (isc.isAn.Array(data)) {
                var useSingleEntry = data.length == 1 && data[0] && data[0].response != null;
                this.logWarn(
                    "RestDataSource transformResponse(): JSON response text is " +
                    "incorrectly formatted as an Array rather than a simple response " +
                    "object."
                    + (useSingleEntry ?
                        " Array contains a single entry which appears to be a validly " +
                        "formatted response object - using this."
                        : "")
                );
                if (useSingleEntry) data = data[0];
            // If we're passed something with no {response:{...}} block log a warning, and
            // continue.
            } else if (data.response == null) {
                this.logWarn("RestDataSouce transformResponse(): JSON response text does " +
                "not appear to be in standard response format.");
            }
            var rawResponse = data.response || {};

            dsResponse.status = this.getValidStatus(rawResponse.status);
        // As invalidateCache is not mandatory field, set a default value of FALSE
        dsResponse.invalidateCache = rawResponse.invalidateCache==null?false:rawResponse.invalidateCache;
            dsResponse.queueStatus = this.getValidStatus(rawResponse.queueStatus);

            // if the status is a validation error, convert the errors from XML
            if (dsResponse.status == isc.DSResponse.STATUS_VALIDATION_ERROR) {
                var errors = rawResponse.errors;
                // Handle being returned an array of errors (per row) or a single error object
                // for the modified row.
                if (isc.isAn.Array(errors)) {
                    if (errors.length > 1) {
                        this.logWarn("server returned an array of errors - ignoring all but the first one");
                    }
                    errors = errors[0];
                }
                dsResponse.errors = errors;
                // if there's also a general error message grab that too
                if (rawResponse.data != null) dsResponse.data = rawResponse.data;
            // handle being passed a failure status with 'data' being an error string to display
            } else if (dsResponse.status < 0) {

                dsResponse.data = rawResponse.data;


                dsResponse.totalRows = dsResponse.startRow;
                dsResponse.endRow = dsResponse.startRow;
            }
            if (rawResponse.totalRows != null) dsResponse.totalRows = rawResponse.totalRows;
            if (rawResponse.startRow != null) dsResponse.startRow = rawResponse.startRow;
            if (rawResponse.endRow != null) dsResponse.endRow = rawResponse.endRow;

        } else {
            if (dsRequest.clientOnly) return dsResponse;
            dsResponse.status = this.getValidStatus(data.selectString("//status"));
        dsResponse.invalidateCache = this.getValidInvalidateCache(data.selectString("//invalidateCache"));
            dsResponse.queueStatus = this.getValidStatus(data.selectString("//queueStatus"));
            // if the status is a validation error, convert the errors from XML
            if (dsResponse.status == isc.DSResponse.STATUS_VALIDATION_ERROR) {
                var errors = data.selectNodes("//errors");
                errors = isc.xml.toJS(errors);
                if (errors.length > 1) {
                    this.logWarn("server returned an array of errors - ignoring all but the first one");
                }
                errors = errors[0];
                dsResponse.errors = errors;
                // if there's also a general error message grab that too
                var errorMessage = data.selectString("//data");
                if (errorMessage) dsResponse.data = errorMessage;
            // handle being passed a raw response where 'data' is an error string to display
            } else if (dsResponse.status < 0) {
                dsResponse.data = data.selectString("//data");
            }

            var totalRows = data.selectNumber("//totalRows");
            if (totalRows != null) dsResponse.totalRows = totalRows;

            var startRow = data.selectNumber("//startRow");
            if (startRow != null) dsResponse.startRow = startRow;

            var endRow = data.selectNumber("//endRow");
            if (endRow != null) dsResponse.endRow = endRow;
        }
        return dsResponse;
    },

    shouldUseTestDataFetch : function () {
        return this.clientOnly == true &&
               this.cacheAllData != true &&
               this.testFileName != null;
    },

    hasTestData : function () {
        return this.testFileName != null;
    },

    getClientOnlyResponse : function (request, serverData) {
        if (request._unserializedData) request.data = request._unserializedData;
        var resp = this.Super("getClientOnlyResponse", arguments);
        if (!isc.isAn.Array(resp.data)) resp.data = [resp.data];
        return resp;
    }


});
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"DataSource",
    addGlobalId:"false",
    Constructor:"DataSource",
    fields:[
        {
            xmlAttribute:"true",
            type:"string",
            name:"ID",
            validators:[
            ]
        },
        {
            title:"Superclass",
            type:"string",
            name:"inheritsFrom",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"useParentFieldOrder",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"useLocalFieldsOnly",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"restrictToParentFields",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"DataFormat",
            valueMap:{
                custom:"Custom Binding",
                iscServer:"ISC Java Server",
                json:"JSON Web Service",
                xml:"XML / WSDL Web Service"
            },
            type:"string",
            name:"dataFormat",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"useStrictJSON",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"noAutoFetch",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Server Type",
            valueMap:{
                custom:"Custom Server Binding",
                sql:"ISC Server SQL Connectors"
            },
            type:"string",
            name:"serverType",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Callback Parameter",
            type:"string",
            name:"callbackParam",
            validators:[
            ]
        },
        {
            type:"Object",
            name:"requestProperties",
            validators:[
            ]
        },
        {
            propertiesOnly:"true",
            childTagName:"field",
            type:"DataSourceField",
            multiple:"true",
            name:"fields",
            validators:[
            ]
        },
        {
            title:"Add Global ID",
            type:"boolean",
            name:"addGlobalId",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"showPrompt",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"DataSource Version",
            visibility:"internal",
            type:"number",
            name:"dataSourceVersion",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Database Name",
            type:"string",
            name:"dbName",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Schema",
            type:"string",
            name:"schema",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Table Name",
            type:"string",
            name:"tableName",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Quote Table Name?",
            type:"boolean",
            name:"quoteTableName",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"tableCode",
            validators:[
            ]
        },
        {
            type:"ServerObject",
            name:"serverObject",
            validators:[
            ]
        },
        {
            type:"OperationBinding",
            multiple:"true",
            name:"operationBindings",
            validators:[
            ]
        },
        {
            hidden:"true",
            propertiesOnly:"true",
            type:"DataSourceField",
            moveTo:"fields",
            name:"field",
            validators:[
            ]
        },
        {
            hidden:"true",
            type:"OperationBinding",
            moveTo:"operationBindings",
            name:"operationBinding",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"serviceNamespace",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"dataURL",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"dataProtocol",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"dataTransport",
            validators:[
            ]
        },
        {
            type:"Object",
            name:"defaultParams",
            validators:[
            ]
        },
        {
            type:"string",
            name:"soapAction",
            validators:[
            ]
        },
        {
            type:"string",
            name:"jsonPrefix",
            validators:[
            ]
        },
        {
            type:"string",
            name:"jsonSuffix",
            validators:[
            ]
        },
        {
            type:"string",
            name:"messageTemplate",
            validators:[
            ]
        },
        {
            visibility:"internal",
            propertiesOnly:"true",
            type:"Object",
            name:"defaultCriteria",
            validators:[
            ]
        },
        {
            visibility:"xmlBinding",
            type:"string",
            name:"tagName",
            validators:[
            ]
        },
        {
            type:"XPath",
            name:"recordXPath",
            validators:[
            ]
        },
        {
            type:"string",
            name:"recordName",
            validators:[
            ]
        },
        {
            type:"Object",
            name:"xmlNamespaces",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"dropExtraFields",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            visibility:"internal",
            type:"string",
            name:"schemaNamespace",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"mustQualify",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xsdSimpleContent",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xsdAnyElement",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xsdAbstract",
            validators:[
            ]
        },
        {
            title:"Title",
            type:"string",
            name:"title",
            validators:[
            ]
        },
        {
            title:"Title Field",
            type:"string",
            name:"titleField",
            validators:[
            ]
        },
        {
            title:"Plural Title",
            type:"string",
            name:"pluralTitle",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Client Only",
            type:"boolean",
            name:"clientOnly",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Test File Name",
            type:"URL",
            name:"testFileName",
            validators:[
            ]
        },
        {
            type:"Object",
            multiple:"true",
            name:"testData",
            validators:[
            ]
        },
        {
            type:"Object",
            multiple:"true",
            name:"cacheData",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"cacheAllData",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"cacheAcrossOperationIds",
            validators:[
            ]
        },
        {
            visibility:"internal",
            propertiesOnly:"true",
            type:"DataSourceField",
            multiple:"true",
            uniqueProperty:"ID",
            name:"types",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"string",
            multiple:"true",
            name:"groups",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"MethodDeclaration",
            multiple:"true",
            name:"methods",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"showSuperClassActions",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"createStandalone",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"useFlatFields",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"showLocalFieldsOnly",
            validators:[
            ]
        },
        {
            type:"Object",
            name:"globalNamespaces",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"autoDeriveSchema",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"autoDeriveFKs",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"useLocalValidators",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"autoDeriveTitles",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"qualifyColumnNames",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"validateRelatedRecords",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"requiresAuthentication",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"requiresRoles",
            validators:[
            ]
        },
        {
            type:"string",
            name:"requires",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"beanClassName",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"autoJoinTransactions",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"sparseUpdates",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"noNullUpdates",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"canExport",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"progressiveLoading",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"autoConvertRelativeDates",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"allowAdvancedCriteria",
            validators:[
            ]
        },
        {
            type:"string",
            name:"scriptImport",
            validators:[
            ]
        },
        {
            idAllowed:"true",
            type:"Object",
            name:"script",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"allowClientRequestedSummaries",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"translatePatternOperators",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"generatedBy",
            validators:[
            ]
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"DataSourceField",
    addGlobalId:"false",
    fields:[
        {
            xmlAttribute:"true",
            title:"Name",
            basic:"true",
            primaryKey:"true",
            type:"string",
            required:"true",
            name:"name",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Type",
            basic:"true",
            type:"string",
            name:"type",
            validators:[
            ]
        },
        {
            title:"Disabled",
            type:"boolean",
            name:"disabled",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"ID Allowed",
            type:"boolean",
            name:"idAllowed",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Required",
            type:"boolean",
            name:"required",
            validators:[
            ]
        },
        {
            type:"ValueMap",
            name:"valueMap",
            validators:[
            ]
        },
        {
            propertiesOnly:"true",
            type:"Validator",
            multiple:"true",
            name:"validators",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Length",
            type:"integer",
            name:"length",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Decimal Pad",
            type:"integer",
            name:"decimalPad",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Decimal Precision",
            type:"integer",
            name:"decimalPrecision",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xmlRequired",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"string",
            name:"xmlMaxOccurs",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"integer",
            name:"xmlMinOccurs",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xmlNonEmpty",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xsElementRef",
            validators:[
            ]
        },
        {
            title:"User can hide",
            type:"boolean",
            name:"canHide",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"xmlAttribute",
            validators:[
            ]
        },
        {
            visibility:"internal",
            type:"boolean",
            name:"mustQualify",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Value XPath",
            type:"XPath",
            name:"valueXPath",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"childrenProperty",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Title",
            type:"string",
            name:"title",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"prompt",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Detail",
            type:"boolean",
            name:"detail",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Can Edit",
            type:"boolean",
            name:"canEdit",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Can Save",
            type:"boolean",
            name:"canSave",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Can View",
            type:"boolean",
            name:"canView",
            validators:[
            ]
        },
        {
            title:"Inapplicable",
            inapplicable:"true",
            type:"boolean",
            name:"inapplicable",
            validators:[
            ]
        },
        {
            title:"Advanced",
            inapplicable:"true",
            type:"boolean",
            name:"advanced",
            validators:[
            ]
        },
        {
            title:"Visibility",
            inapplicable:"true",
            type:"string",
            name:"visibility",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Hidden",
            inapplicable:"true",
            type:"boolean",
            name:"hidden",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Is Primary Key",
            type:"boolean",
            name:"primaryKey",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Foreign Key",
            type:"string",
            name:"foreignKey",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Tree Root Value",
            type:"string",
            name:"rootValue",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Include From",
            type:"string",
            name:"includeFrom",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Include Via",
            type:"string",
            name:"includeVia",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Related Table Alias",
            type:"string",
            name:"relatedTableAlias",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"showFileInline",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"escapeHTML",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"integerOrIdentifier",
            name:"imageWidth",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"integerOrIdentifier",
            name:"imageHeight",
            validators:[
            ]
        },
        {
            title:"Native Name",
            hidden:"true",
            type:"string",
            name:"nativeName",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Native Foreign Key",
            hidden:"true",
            type:"string",
            name:"nativeFK",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            hidden:"true",
            type:"boolean",
            name:"autoDeriveFKs",
            validators:[
            ]
        },
        {
            title:"Field Name",
            hidden:"true",
            type:"string",
            name:"fieldName",
            validators:[
            ]
        },
        {
            hidden:"true",
            propertiesOnly:"true",
            childTagName:"field",
            type:"DataSourceField",
            multiple:"true",
            uniqueProperty:"name",
            name:"fields",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"multiple",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"validateEachItem",
            validators:[
            ]
        },
        {
            type:"Object",
            multiple:"true",
            name:"pickListFields",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"canFilter",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"ignore",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"unknownType",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"canSortClientOnly",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"childTagName",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"basic",
            validators:[
            ]
        },
        {
            type:"integer",
            name:"maxFileSize",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            title:"Frozen",
            type:"boolean",
            name:"frozen",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"canExport",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"sqlStorageStrategy",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"encodeInResponse",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"mimeType",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"ignoreTextMatchStyle",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"boolean",
            name:"lenientXPath",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"summaryFunction",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"joinString",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"joinPrefix",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"joinSuffix",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"allowClientRequestedSummaries",
            validators:[
            ]
        },
        {
            xmlAttribute:"true",
            type:"string",
            name:"columnCode",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"nillable",
            validators:[
            ]
        },
        {
            type:"string",
            name:"validOperators",
            validators:[
            ]
        },
        {
            type:"string",
            name:"fieldTypeProperty",
            validators:[
            ]
        },
        {
            type:"string",
            name:"moveTo",
            validators:[
            ]
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"Validator",
    addGlobalId:"false",
    fields:[
        {
            type:"string",
            name:"type",
            validators:[
            ]
        },
        {
            type:"string",
            name:"name",
            validators:[
            ]
        },
        {
            type:"string",
            name:"description",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"stopIfFalse",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"stopOnError",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"validateOnChange",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"clientOnly",
            validators:[
            ]
        },
        {
            type:"string",
            name:"errorMessage",
            validators:[
            ]
        },
        {
            type:"float",
            name:"max",
            validators:[
            ]
        },
        {
            type:"float",
            name:"min",
            validators:[
            ]
        },
        {
            type:"boolean",
            name:"exclusive",
            validators:[
            ]
        },
        {
            type:"regexp",
            name:"mask",
            validators:[
            ]
        },
        {
            type:"regexp",
            name:"transformTo",
            validators:[
            ]
        },
        {
            type:"integer",
            name:"precision",
            validators:[
            ]
        },
        {
            type:"string",
            name:"expression",
            validators:[
            ]
        },
        {
            type:"string",
            name:"otherField",
            validators:[
            ]
        },
        {
            type:"text",
            multiple:"true",
            name:"list",
            validators:[
            ]
        },
        {
            type:"ValueMap",
            name:"valueMap",
            validators:[
            ]
        },
        {
            type:"text",
            name:"substring",
            validators:[
            ]
        },
        {
            type:"text",
            name:"operator",
            validators:[
            ]
        },
        {
            type:"integer",
            name:"count",
            validators:[
            ]
        },
        {
            type:"AdvancedCriteria",
            name:"applyWhen",
            validators:[
            ]
        },
        {
            type:"string",
            multiple:"true",
            name:"dependentFields",
            validators:[
            ]
        },
        {
            type:"string",
            name:"scriptImport",
            validators:[
            ]
        },
        {
            idAllowed:"true",
            type:"Object",
            name:"serverCondition",
            validators:[
            ]
        },
        {
            type:"ServerObject",
            name:"serverObject",
            validators:[
            ]
        },
        {
            type:"string",
            name:"fieldName",
            validators:[
            ]
        },
        {
            type:"string",
            name:"triggerEvent",
            validators:[
            ]
        },
        {
            type:"string",
            name:"message",
            validators:[
            ]
        },
        {
            type:"string",
            name:"severity",
            validators:[
            ]
        },
        {
            type:"string",
            name:"displayMode",
            validators:[
            ]
        },
        {
            type:"integer",
            name:"duration",
            validators:[
            ]
        },
        {
            type:"string",
            name:"formula",
            validators:[
            ]
        },
        {
            type:"ValueMap",
            name:"formulaVars",
            validators:[
            ]
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"SimpleType",
    addGlobalId:false,
    Constructor:"SimpleType",
    fields:[
        {
            validators:[
            ],
            name:"inheritsFrom",
            type:"string"
        },
        {
            validators:[
            ],
            name:"editorType",
            type:"string"
        }
    ],
    inheritsFrom:"DataSourceField"
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"XSComplexType",
    addGlobalId:false,
    Constructor:"XSComplexType",
    fields:[
    ],
    inheritsFrom:"DataSource"
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"XSElement",
    addGlobalId:false,
    Constructor:"XSElement",
    fields:[
    ],
    inheritsFrom:"DataSource"
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"SchemaSet",
    addGlobalId:false,
    Constructor:"SchemaSet",
    fields:[
        {
            validators:[
            ],
            name:"schemaNamespace",
            type:"url"
        },
        {
            validators:[
            ],
            name:"schemaImports",
            type:"Object",
            multiple:true
        },
        {
            validators:[
            ],
            name:"qualifyAll",
            type:"boolean"
        },
        {
            validators:[
            ],
            name:"schema",
            type:"DataSource",
            multiple:true
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"WSDLMessage",
    addGlobalId:false,
    Constructor:"WSDLMessage",
    fields:[
    ],
    inheritsFrom:"DataSource"
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"WebService",
    addGlobalId:false,
    Constructor:"WebService",
    fields:[
        {
            validators:[
            ],
            name:"location",
            type:"url"
        },
        {
            validators:[
            ],
            name:"targetNamespace",
            type:"url"
        },
        {
            validators:[
            ],
            name:"schemaImports",
            type:"Object",
            multiple:true
        },
        {
            validators:[
            ],
            name:"wsdlImports",
            type:"Object",
            multiple:true
        },
        {
            validators:[
            ],
            name:"operations",
            type:"WebServiceOperation",
            multiple:true
        },
        {
            validators:[
            ],
            name:"portTypes",
            type:"Object",
            multiple:true
        },
        {
            validators:[
            ],
            name:"bindings",
            type:"Object",
            multiple:true
        },
        {
            validators:[
            ],
            name:"messages",
            type:"WSDLMessage",
            multiple:true
        },
        {
            validators:[
            ],
            name:"globalNamespaces",
            type:"Object"
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"WebServiceOperation",
    addGlobalId:false,
    fields:[
        {
            title:"Operation Name",
            validators:[
            ],
            name:"name",
            required:true
        },
        {
            title:"SOAPAction Header",
            validators:[
            ],
            name:"soapAction"
        },
        {
            title:"Input Message",
            validators:[
            ],
            name:"inputMessage"
        },
        {
            title:"Output Message",
            validators:[
            ],
            name:"outputMessage"
        },
        {
            validators:[
            ],
            name:"inputHeaders",
            type:"WSOperationHeader",
            multiple:true
        },
        {
            validators:[
            ],
            name:"outputHeaders",
            type:"WSOperationHeader",
            multiple:true
        }
    ]
})
isc.DataSource.create({
    allowAdvancedCriteria:true,
    ID:"WSOperationHeader",
    addGlobalId:false,
    fields:[
        {
            validators:[
            ],
            name:"encoding"
        },
        {
            validators:[
            ],
            name:"message"
        },
        {
            validators:[
            ],
            name:"part"
        }
    ]
})






//> @class Operators
// The Operators class provides a collection of class-level properties that can be used to
// modify the descriptions associated with the logical +link{operator}s used in
// +link{AdvancedCriteria}. This is primarily intended for internationalization.
// <p>
// To add or override an operator's description, use
// Operators.+link{Class.addClassProperties,addClassProperties()}.  For example:
// <p>
// <code>Operators.addClassProperties({lessOrEqualTitle: "Less than or equal to"});</code>
// @treeLocation Client Reference/Data Binding/DataSource
// @visibility external
//<
isc.defineClass("Operators", "Class").addClassProperties({

    //> @classAttr Operators.equalsTitle (String : "equals" : IR)
    // Title for the "equals" operator
    // @group i18nMessages
    // @visibility external
    //<
    equalsTitle: "equals",

    //> @classAttr Operators.notEqualTitle (String : "not equal" : IR)
    // Title for the "notEqual" operator
    // @group i18nMessages
    // @visibility external
    //<
    notEqualTitle: "not equal",

    //> @classAttr Operators.iEqualsTitle (String : "equals (ignore case)" : IR)
    // Title for the "iEquals" operator
    // @group i18nMessages
    // @visibility external
    //<
    iEqualsTitle: "equals (ignore case)",

    //> @classAttr Operators.iNotEqualTitle (String : "not equal (ignore case)" : IR)
    // Title for the "iNotEqual" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotEqualTitle: "not equal (ignore case)",

    //> @classAttr Operators.greaterThanTitle (String : "greater than" : IR)
    // Title for the "greaterThan" operator
    // @group i18nMessages
    // @visibility external
    //<
    greaterThanTitle: "greater than",

    //> @classAttr Operators.lessThanTitle (String : "less than" : IR)
    // Title for the "lessThan" operator
    // @group i18nMessages
    // @visibility external
    //<
    lessThanTitle: "less than",

    //> @classAttr Operators.greaterOrEqualTitle (String : "greater than or equal to" : IR)
    // Title for the "greaterOrEqual" operator
    // @group i18nMessages
    // @visibility external
    //<
    greaterOrEqualTitle: "greater than or equal to",

    //> @classAttr Operators.lessOrEqualTitle (String : "less than or equal to" : IR)
    // Title for the "lessOrEqual" operator
    // @group i18nMessages
    // @visibility external
    //<
    lessOrEqualTitle: "less than or equal to",

    //> @classAttr Operators.betweenTitle (String : "between (match case)" : IR)
    // Title for the "between" operator
    // @group i18nMessages
    // @visibility external
    //<
    betweenTitle: "between (match case)",

    //> @classAttr Operators.iBetweenTitle (String : "between" : IR)
    // Title for the "iBetween" operator
    // @group i18nMessages
    // @visibility external
    //<
    iBetweenTitle: "between",

    //> @classAttr Operators.betweenInclusiveTitle (String : "between (inclusive, match case)" : IR)
    // Title for the "betweenInclusive" operator
    // @group i18nMessages
    // @visibility external
    //<
    betweenInclusiveTitle: "between (inclusive, match case)",

    //> @classAttr Operators.iBetweenInclusiveTitle (String : "between (inclusive)" : IR)
    // Title for the "iBetweenInclusive" operator
    // @group i18nMessages
    // @visibility external
    //<
    iBetweenInclusiveTitle: "between (inclusive)",

    //> @classAttr Operators.iContainsTitle (String : "contains" : IR)
    // Title for the "iContains" operator
    // @group i18nMessages
    // @visibility external
    //<
    iContainsTitle: "contains",

    //> @classAttr Operators.iStartsWithTitle (String : "starts with" : IR)
    // Title for the "iStartsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    iStartsWithTitle: "starts with",

    //> @classAttr Operators.iEndsWithTitle (String : "ends with" : IR)
    // Title for the "iEndsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    iEndsWithTitle: "ends with",

    //> @classAttr Operators.containsTitle (String : "contains (match case)" : IR)
    // Title for the "contains" operator
    // @group i18nMessages
    // @visibility external
    //<
    containsTitle: "contains (match case)",

    //> @classAttr Operators.startsWithTitle (String : "starts with (match case)" : IR)
    // Title for the "startsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    startsWithTitle: "starts with (match case)",

    //> @classAttr Operators.endsWithTitle (String : "ends with (match case)" : IR)
    // Title for the "endsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    endsWithTitle: "ends with (match case)",

    //> @classAttr Operators.iNotContainsTitle (String : "does not contain" : IR)
    // Title for the "iNotContains" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotContainsTitle: "does not contain",

    //> @classAttr Operators.iNotStartsWithTitle (String : "does not start with" : IR)
    // Title for the "iNotStartsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotStartsWithTitle: "does not start with",

    //> @classAttr Operators.iNotEndsWithTitle (String : "does not end with" : IR)
    // Title for the "iNotEndsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotEndsWithTitle: "does not end with",

    //> @classAttr Operators.notContainsTitle (String : "does not contain (match case)" : IR)
    // Title for the "notContains" operator
    // @group i18nMessages
    // @visibility external
    //<
    notContainsTitle: "does not contain (match case)",

    //> @classAttr Operators.notStartsWithTitle (String : "does not start with (match case)" : IR)
    // Title for the "notStartsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    notStartsWithTitle: "does not start with (match case)",

    //> @classAttr Operators.notEndsWithTitle (String : "does not end with (match case)" : IR)
    // Title for the "notEndsWith" operator
    // @group i18nMessages
    // @visibility external
    //<
    notEndsWithTitle: "does not end with (match case)",

    //> @classAttr Operators.isNullTitle (String : "is null" : IR)
    // Title for the "isNull" operator
    // @group i18nMessages
    // @visibility external
    //<
    isNullTitle: "is null",

    //> @classAttr Operators.notNullTitle (String : "is not null" : IR)
    // Title for the "notNull" operator
    // @group i18nMessages
    // @visibility external
    //<
    notNullTitle: "not null",

    //> @classAttr Operators.regexpTitle (String : "matches expression (exact case)" : IR)
    // Title for the "regexp" operator
    // @group i18nMessages
    // @visibility external
    //<
    regexpTitle: "matches expression (exact case)",

    //> @classAttr Operators.iregexpTitle (String : "matches expression" : IR)
    // Title for the "iregexp" operator
    // @group i18nMessages
    // @visibility external
    //<
    iregexpTitle: "matches expression",

    //> @classAttr Operators.matchesPatternTitle (String : "matches pattern (exact case)" : IR)
    // Title for the "matchesPattern" operator
    // @group i18nMessages
    // @visibility external
    //<
    matchesPatternTitle: "matches pattern (exact case)",

    //> @classAttr Operators.iMatchesPatternTitle (String : "matches pattern" : IR)
    // Title for the "iMatchesPattern" operator
    // @group i18nMessages
    // @visibility external
    //<
    iMatchesPatternTitle: "matches pattern",

    //> @classAttr Operators.containsPatternTitle (String : "contains pattern (exact case)" : IR)
    // Title for the "containsPattern" operator
    // @group i18nMessages
    // @visibility external
    //<
    containsPatternTitle: "contains pattern (exact case)",

    //> @classAttr Operators.iContainsPatternTitle (String : "contains pattern" : IR)
    // Title for the "iContainsPattern" operator
    // @group i18nMessages
    // @visibility external
    //<
    iContainsPatternTitle: "contains pattern",

    //> @classAttr Operators.inSetTitle (String : "is one of" : IR)
    // Title for the "inSet" operator
    // @group i18nMessages
    // @visibility external
    //<
    inSetTitle: "is one of",

    //> @classAttr Operators.notInSetTitle (String : "is not one of" : IR)
    // Title for the "notInSet" operator
    // @group i18nMessages
    // @visibility external
    //<
    notInSetTitle: "is not one of",

    //> @classAttr Operators.equalsFieldTitle (String : "matches other field (match case)" : IR)
    // Title for the "equalsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    equalsFieldTitle: "matches other field (match case)",

    //> @classAttr Operators.notEqualFieldTitle (String : "differs from field (match case)" : IR)
    // Title for the "notEqualField" operator
    // @group i18nMessages
    // @visibility external
    //<
    notEqualFieldTitle: "differs from field (match case)",

    //> @classAttr Operators.iEqualsFieldTitle (String : "matches other field (case insensitive)" : IR)
    // Title for the "iEqualsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iEqualsFieldTitle: "matches other field (case insensitive)",

    //> @classAttr Operators.iNotEqualFieldTitle (String : "differs from field (case insensitive)" : IR)
    // Title for the "iNotEqualField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotEqualFieldTitle: "differs from field (case insensitive)",

    //> @classAttr Operators.greaterThanFieldTitle (String : "greater than field" : IR)
    // Title for the "greaterThanField" operator
    // @group i18nMessages
    // @visibility external
    //<
    greaterThanFieldTitle: "greater than field",

    //> @classAttr Operators.lessThanFieldTitle (String : "less than field" : IR)
    // Title for the "lessThanField" operator
    // @group i18nMessages
    // @visibility external
    //<
    lessThanFieldTitle: "less than field",

    //> @classAttr Operators.greaterOrEqualFieldTitle (String : "greater than or equal to field" : IR)
    // Title for the "greaterOrEqualField" operator
    // @group i18nMessages
    // @visibility external
    //<
    greaterOrEqualFieldTitle: "greater than or equal to field",

    //> @classAttr Operators.lessOrEqualFieldTitle (String : "less than or equal to field" : IR)
    // Title for the "lessOrEqualField" operator
    // @group i18nMessages
    // @visibility external
    //<
    lessOrEqualFieldTitle: "less than or equal to field",

    //> @classAttr Operators.containsFieldTitle (String : "contains (match case) another field value" : IR)
    // Title for the "containsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    containsFieldTitle: "contains (match case) another field value",

    //> @classAttr Operators.startsWithFieldTitle (String : "starts with (match case) another field value" : IR)
    // Title for the "startsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    startsWithFieldTitle: "starts with (match case) another field value",

    //> @classAttr Operators.endsWithFieldTitle (String : "ends with (match case) another field value" : IR)
    // Title for the "endsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    endsWithFieldTitle: "ends with (match case) another field value",

    //> @classAttr Operators.iContainsFieldTitle (String : "contains (case insensitive) another field value" : IR)
    // Title for the "iContainsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iContainsFieldTitle: "contains (case insensitive) another field value",

    //> @classAttr Operators.iStartsWithFieldTitle (String : "starts with (case insensitive) another field value" : IR)
    // Title for the "iStartsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iStartsWithFieldTitle: "starts with (case insensitive) another field value",

    //> @classAttr Operators.iEndsWithFieldTitle (String : "ends with (case insensitive) another field value" : IR)
    // Title for the "iEndsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iEndsWithFieldTitle: "ends with (case insensitive) another field value",

    //> @classAttr Operators.notContainsFieldTitle (String : "does not contain (match case) another field value" : IR)
    // Title for the "notContainsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    notContainsFieldTitle: "does not contain (match case) another field value",

    //> @classAttr Operators.notStartsWithFieldTitle (String : "does not start with (match case) another field value" : IR)
    // Title for the "notStartsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    notStartsWithFieldTitle: "does not start with (match case) another field value",

    //> @classAttr Operators.notEndsWithFieldTitle (String : "does not end with (match case) another field value" : IR)
    // Title for the "notEndsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    notEndsWithFieldTitle: "does not end with (match case) another field value",

    //> @classAttr Operators.iNotContainsFieldTitle (String : "does not contain (case insensitive) another field value" : IR)
    // Title for the "iNotContainsField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotContainsFieldTitle: "does not contain (case insensitive) another field value",

    //> @classAttr Operators.iNotStartsWithFieldTitle (String : "does not start with (case insensitive) another field value" : IR)
    // Title for the "iNotStartsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotStartsWithFieldTitle: "does not start with (case insensitive) another field value",

    //> @classAttr Operators.iNotEndsWithFieldTitle (String : "does not end with (case insensitive) another field value" : IR)
    // Title for the "iNotEndsWithField" operator
    // @group i18nMessages
    // @visibility external
    //<
    iNotEndsWithFieldTitle: "does not end with (case insensitive) another field value",

    //> @classAttr Operators.andTitle (String : "Match All" : IR)
    // Title for the "and" operator
    // @group i18nMessages
    // @visibility external
    //<
    andTitle: "and",

    //> @classAttr Operators.notTitle (String : "Match None" : IR)
    // Title for the "not" operator
    // @group i18nMessages
    // @visibility external
    //<
    notTitle: "not",

    //> @classAttr Operators.orTitle (String : "Match Any" : IR)
    // Title for the "or" operator
    // @group i18nMessages
    // @visibility external
    //<
    orTitle: "or"

});


// FilterBuilder requires DynamicForm but is loaded in a separate module.
// Ensure DF is present before attempting to initialize
if (isc.DynamicForm) {



// This class is used by the clause forms that make up a filter-builder.
// Extend SearchForm rather than DynamicForm - this ensures we show editableItems if
// canFilter is true.
isc.defineClass("DynamicFilterForm", "SearchForm");
isc.DynamicFilterForm.addProperties({

    _$Enter:"Enter",
    handleKeyPress : function (event, eventInfo) {
        // We need to suppress normal DynamicForm saveOnEnter behavior; we also need to let
        // the FilterBuilder that will eventually see this event know whether or not the field
        // triggering it was a TextItem
        var item = this.getFocusSubItem();
        if (isc.isA.TextItem(item)) eventInfo.firedOnTextItem = true;

        // But we need normal key handling for everything except Enter!
        if (event.keyName != this._$Enter) {
            return this.Super("handleKeyPress", [event, eventInfo]);
        }
    },
    itemChanged : function (item, newValue, oldValue) {
        if (this.creator.itemChanged) this.creator.itemChanged();
    },

    getDefaultOptionDataSource : function (field) {
        if (this.creator && this.creator.getDefaultOptionDataSource) {
            return this.creator.getDefaultOptionDataSource(field);
        }
        return this.Super("getDefaultOptionDataSource", arguments);
    }
});



//> @class FilterClause
// A horizontal, Layout-based widget that allows a user to input a single criterion based on
// one field and one operator.
// <P>
// Note that FilterClauses must be used in conjunction with a +link{class:filterBuilder}. By default
// the FilterBuilder will auto-generate its clauses based on specified criteria, but for advanced
// usage a FilterClause may be instantiated directly and passed to a filterBuilder via
// +link{filterBuilder.addClause()}.
//
// @treeLocation Client Reference/Forms
// @visibility external
//<

isc.defineClass("FilterClause", "Layout").addProperties({
// props from HLayout
orientation:"horizontal",
defaultWidth: 20,
height: 20,

//> @attr filterClause.criterion (Criteria : null : IRW)
// Initial criterion for this FilterClause.
// <P>
// When initialized with a criterion, the clause will be automatically set up for editing
// the supplied criterion.
// <P>
// Note that an empty or partial criterion is allowed, for example, it may specify
// +link{criterion.fieldName} only and will generate an expression with the operator not chosen.
// @visibility external
//<

//> @attr filterClause.showFieldTitles (Boolean : true : IR)
// If true (the default), show field titles in the drop-down box used to select a field for querying.
// If false, show actual field names instead.
// @visibility external
//<
showFieldTitles: true,

//> @attr filterClause.validateOnChange (Boolean : true : IR)
// If true (the default), validates the entered value when it changes, to make sure it is a
// a valid value of its type (valid string, number, and so on).  No other validation is
// carried out.  If you switch this property off, it is still possible to validate the
// <code>FilterClause</code> by calling +link{filterClause.validate()} from your own code.
// @visibility external
//<
validateOnChange: true,

// Clause creation
// ---------------------------------------------------------------------------------------

// Note that fieldPicker, operatorPicker and valueItem defaults and properties
// may be overridden at the filterBuilder level
fieldPickerWidth: "*",
operatorPickerWidth: 150,
valueItemWidth: 150,

fieldPickerDefaults: {
    type: "SelectItem",
    name: "fieldName",
    showTitle: false,
    textMatchStyle: "startsWith",
    changed : function () { this.form.creator.fieldNameChanged(this.form); }
},

//> @attr filterClause.fieldPicker (AutoChild PickList : null : IR)
// @include filterBuilder.fieldPicker
//
// @visibility external
//<


//> @attr filterClause.fieldPickerTitle (String : "Field Name" : IR)
// The title for the +link{fieldPicker, field-picker} select-item.
// @group i18nMessages
// @visibility external
//<
fieldPickerTitle: "Field Name",


//> @attr filterClause.fieldPickerProperties (FormItem Properties : null : IR)
// Properties to combine with the +link{fieldPicker} autoChild FormItem.
//
// @visibility external
//<

//> @attr filterClause.operatorPicker (AutoChild SelectItem : null : IR)
// @include filterBuilder.operatorPicker
//
// @visibility external
//<

//> @attr filterClause.operatorPickerProperties (FormItem Properties : null : IR)
// Properties to combine with the +link{operatorPicker} autoChild FormItem.
//
// @visibility external
//<
operatorPickerDefaults : {
    // list of operators
    name:"operator",
    type:"select",
    showTitle:false,
    // don't allow addUnknownValues - it's a fixed list applicable to the selected field
    addUnknownValues:false,
    defaultToFirstOption:true,
    changed : function () { this.form.creator.operatorChanged(this.form); }
},

//> @attr filterClause.operatorPickerTitle (String : "Operator" : IR)
// The title for the operator-picker select-item.
// @group i18nMessages
// @visibility external
//<
operatorPickerTitle: "Operator",


//> @attr filterClause.valueItemTitle (String : "Value" : IR)
// The title for the value-item.
// @group i18nMessages
// @visibility external
//<
valueItemTitle: "Value",

//> @attr filterClause.valueSetHint (String : "Comma-separated values" : IR)
// A hint to show in the value-item when editing an operator which takes an array of values.
// @group i18nMessages
// @visibility external
//<
valueSetHint:"Comma-separated values",


//> @attr filterClause.clause (AutoChild SearchForm : null : IR)
// AutoChild containing the UI for the filter-properties in this FilterClause.
// @visibility external
//<
clauseConstructor: isc.DynamicFilterForm,

//> @attr filterClause.showRemoveButton (Boolean : true : IR)
// If set, show a button for this clause allowing it to be removed.
// @visibility external
//<
showRemoveButton:true,

//> @attr filterClause.removeButtonPrompt (string : "Remove" : IR)
// The hover prompt text for the remove button.
//
// @group i18nMessages
// @visibility external
//<
removeButtonPrompt: "Remove",

// set this flag to prevent non-filterable fields from being excluded - such exclusion makes
// sense in a FilterBuilder, but we don't want it when we're using a FilterClause widget simply
// as a UI - for instance, from the HiliteEditor.
excludeNonFilterableFields: true,

//> @attr filterClause.removeButton (AutoChild ImgButton : null : IR)
// The clause removal ImgButton that appears before this clause if
// +link{showRemoveButton} is set.
// @visibility external
//<
removeButtonDefaults : {
    _constructor:isc.ImgButton,
    width:18, height:18, layoutAlign:"center",
    src:"[SKIN]/actions/remove.png",
    showRollOver:false, showDown:false,
    showDisabled:false, // XXX
    click: function () { this.creator.remove(); }
},

flattenItems: true

});

isc.FilterClause.addMethods({

initWidget : function () {
    this.Super("initWidget", arguments);
    this.setupClause();
},

//> @method filterClause.getFilterBuilder()
// Returns the +link{class:filterBuilder,filterBuilder} containing this clause, or null if
// this filterClause is not embedded in a filterBuilder.
// @visibility external
//<
getFilterBuilder : function () {
    // filterBuilder attribute is set by filterBuilder.addClause()
    return this.filterBuilder;
},

// getPrimaryDS() - this returns the dataSource used to drive field operators etc.
getPrimaryDS : function (fieldName) {
    if (this.dataSources) {

        if (fieldName == null) {
            return isc.DataSource.get(this.dataSources[0]);
        }

        if (isc.isAn.Object(fieldName)) {
            if (fieldName._origField != null) {
                fieldName = fieldName._origField;
            }
            for (var i = 0; i < this.dataSources.length; i++) {
                var fields = isc.getValues(this.dataSources[i].getFields());
                if (fields.contains(fieldName)) return this.dataSources[i];
            }
            // it's an object but wasn't an actual field within a DS, grab the name

            fieldName = fieldName.name;
        }

        return isc.DataSource.getDataSourceForField(fieldName, this.dataSources);
    }
    if (this.dataSource) return this.getDataSource();
    else if (this.fieldDataSource) return this.fieldDataSource;
},

getDefaultOptionDataSource : function (field) {
    return this.getPrimaryDS(field);
},

getField : function (fieldName) {

    if (fieldName == null) return null;

    if (isc.isAn.Object(fieldName)) return fieldName;
    var field;
    if (this.dataSources) {
        field = isc.DataSource.getFieldFromDataSources(fieldName, this.dataSources);
    } else if (this.dataSource) {
        field = this.getDataSource().getField(fieldName);
        if (field == null) {
            field = this.getDataSource().getFieldForDataPath(fieldName);
        }
    } else {
        if (this.clause) {
            field = this.fieldData ? this.fieldData[fieldName] : null;
            if (!field) field = this.clause.getField("fieldName").getSelectedRecord();
            if (!field) field = this.field;
            else this.field = field;
        }
    }
    return field;
},

getFieldNames : function () {
    if (this.dataSources) {
        return isc.DataSource.getCombinedDataSourceFields(this.dataSources);
    }
    if (this.dataSource) return this.getDataSource().getFieldNames(true);
},

getFieldOperatorMap : function (field, includeHidden, valueType, omitValueType) {
    // call the local getFieldOperators() method, which may be overridden, and pass the resulting
    // list into ds.getFieldOperatorMap() as new undoc'd final param "operators"
    var operators = this.getFieldOperators(field),
        ds = this.getPrimaryDS(field),
        map = ds.getFieldOperatorMap(field, includeHidden, valueType, omitValueType, operators)
    ;
    return map;
},

getSearchOperator : function (operatorName, field) {
    return this.getPrimaryDS(field).getSearchOperator(operatorName);
},

combineFieldData : function (field, targetField) {
    var ds = this.getPrimaryDS(targetField),
        dsField = this.getField(targetField);
    if (dsField)
        return ds.combineFieldData(field, targetField);
    else return field;
},

// setupClause initializes autoChildren etc.
setupClause : function () {
    this._clauseInitialized = true;
    if (this.dataSource && !isc.isA.DataSource(this.dataSource))
        this.dataSource = isc.DataSource.get(this.dataSource);
    if (this.fieldDataSource && !isc.isA.DataSource(this.fieldDataSource))
        this.fieldDataSource = isc.DataSource.get(this.fieldDataSource);

    this.fieldPickerDefaults.title = this.fieldPickerTitle;
    this.operatorPickerDefaults.title = this.operatorPickerTitle;

    var fieldMap = {};

    if (this.showClause != false) {
        if (this.topOperatorAppearance == "inline") {
            if (this.topOperator == "and") {
                var map = {and: this.creator.inlineAndTitle, not: this.creator.inlineAndNotTitle};
            } else {
                var map = {or: this.creator.inlineOrTitle, not: this.creator.inlineAndNotTitle};
            }
            var operatorIndex = 0;
            this.topOperatorFormProperties = {layoutAlign: "top"};
            if (this.creator.showSelectionCheckbox) {
                isc.addProperties(this.topOperatorFormProperties, {
                    numCols: 2,
                    width: 120,
                    colWidths: ["20%", "80%"]
                });

                this.topOperatorFormProperties.items = [
                    // Should be extracted into a configurable auto-child type block
                    {
                    name: "select",
                    type: "checkbox",
                    showTitle: false,
                    showLabel: false,
                    defaultValue: false,
                    showIf: function() { return this.form.creator.showSelectionCheckbox }
                    },
                    isc.addProperties(
                        {width:this.topOperatorItemWidth},
                        this.topOperatorItemDefaults, this.topOperatorItemProperties
                    )
                ];
                operatorIndex = 1;
            } else {
                this.topOperatorFormProperties.items = [
                    isc.addProperties(
                        {width:this.topOperatorItemWidth},
                        this.topOperatorItemDefaults, this.topOperatorItemProperties
                    )
                ];
            }
            this.addAutoChild("topOperatorForm");
            this.topOperatorForm.items[operatorIndex].valueMap = map;
            this.topOperatorForm.items[operatorIndex].defaultValue = this.negated ?
                                                                     "not" : this.topOperator;
            this.updateInlineTopOperator();
        }
        // set up the items - field and operator pickers
        var items = [];
        items.add(isc.addProperties({}, this.fieldPickerDefaults,
            { width: this.fieldPickerWidth,
              sortField: (this.sortFields ? "fieldName" : null) },
            this.fieldPickerProperties,
            {name:"fieldName"}
        ));
        // only show the operator item if the field is not missing - if the field IS missing,
        // extend the text shown as the field-title to show an readable explanation instead
        items.add(isc.addProperties({}, this.operatorPickerDefaults,
            { width: this.operatorPickerWidth },
            this.operatorPickerProperties,
            { name:"operator", showIf: this.missingField ? "false" : "true" }
        ));
        var criterion = this.criterion,
            fieldNames = this.getFieldNames(),
            selectedFieldName
        ;

        if (this.fieldName && (this.dataSource || this.dataSources)) {
            // fieldName provided - change the type of the first DF field and set it's value -
            // this behavior is only supported when this.dataSource is present
            var specificFieldName = this.fieldName;
            var field = this.getField(specificFieldName),
                fieldTitle
            ;
            isc.addProperties(items[0], { type: "staticText", clipValue: true, wrap: false });

            if (this.missingField) {
                fieldTitle = isc.DataSource.getCriterionDescription(this.criterion, this.dataSource || this.dataSources);
            } else if (!field || (this.excludeNonFilterableFields && field.canFilter == false)) {
                specificFieldName = fieldNames[0];
            } else if (this.showFieldTitles) {
                fieldTitle = field.summaryTitle || field.title || specificFieldName;
            }
            items[0].defaultValue = specificFieldName;
            if (fieldTitle != null) {
                var valueMap = {};
                valueMap[specificFieldName] = fieldTitle;
                items[0].valueMap = valueMap;
            }
            selectedFieldName = specificFieldName;
        } else {
            if (this.fieldDataSource) {
                // change the fieldPicker to be a ComboBoxItem, setup the fieldDataSource as
                // it's optionDataSource and provide type-ahead auto-completion
                isc.addProperties(items[0], {
                    type: "ComboBoxItem",
                    completeOnTab: true,
                    addUnknownValues:false,
                    optionDataSource: this.fieldDataSource,
                    valueField: "name",
                    displayField: this.showFieldTitles ? "title" : "name",

                    pickListProperties: { reusePickList : function () { return false;} }
                });
                if (this.field) {
                    items[0].defaultValue = items[0].defaultValue || this.field.name;
                }
            } else if (fieldNames) {
                // build and assign a valueMap to the fieldPicker item
                for (var i = 0; i < fieldNames.length; i++) {
                    var fieldName = fieldNames[i],
                        field = this.getField(fieldName);
                    if (this.excludeNonFilterableFields && field.canFilter == false) continue;
                    if (this.showFieldTitles) {
                        fieldMap[fieldName] = field.summaryTitle || field.title || fieldName;
                    } else {
                        fieldMap[fieldName] = fieldName;
                    }
                }
                items[0].valueMap = fieldMap;

                items[0].defaultValue = items[0].defaultValue || isc.firstKey(fieldMap);
            }
        }

        this.fieldPicker = items[0];
        var fieldItem = items[0],
            operatorItem = items[1];

        if (!this.fieldName) {
            if (criterion && criterion.fieldName) {
                if (this.fieldDataSource) {
                    fieldItem.defaultValue = criterion.fieldName;
                } else {
                    if (fieldNames.contains(criterion.fieldName)) {
                        fieldItem.defaultValue = criterion.fieldName;
                    } else {
                        isc.logWarn("Criterion specified field " + criterion.fieldName + ", which is not" +
                                    " in the record. Using the first record field (" +
                                    (fieldMap ? isc.firstKey(fieldMap) : fieldNames[0]) +
                                    ") instead");
                        fieldItem.defaultValue = fieldMap ? isc.firstKey(fieldMap) : fieldNames[0];
                    }
                }
            }

            selectedFieldName = fieldItem.defaultValue;
        }
        if (selectedFieldName) {
            var field = this.field || this.getField(selectedFieldName);

            if (!this.missingField && field) {
                var valueMap = field ? this.getFieldOperatorMap(field, false, "criteria", true) : null;

                operatorItem.valueMap = valueMap;
                if (valueMap) {
                    if (criterion && criterion.operator) {
                        operatorItem.defaultValue = criterion.operator;
                    } else {
                        operatorItem.defaultValue = isc.firstKey(valueMap);
                    }
                }

                this._lastFieldName = selectedFieldName;

                var operator = this.getSearchOperator(operatorItem.defaultValue, field);

                if (!operator && valueMap.length > 0) {
                    isc.logWarn("Criterion specified unknown operator " +
                            (criterion ? criterion.operator : "[null criterion]") +
                            ". Using the first valid operator (" + isc.firstKey(valueMap) + ") instead");
                    operatorItem.defaultValue = isc.firstKey(valueMap);
                    operator = this.getSearchOperator(operatorItem.defaultValue, field);
                }
                var valueItems = this.buildValueItemList(field, operator, selectedFieldName);


                if (criterion) {
                    if (criterion.value != null && valueItems.containsProperty("name", "value")) {
                        valueItems.find("name", "value").defaultValue = criterion.value;
                    }
                    if (criterion.start != null && valueItems.containsProperty("name", "start")) {
                        valueItems.find("name", "start").defaultValue = criterion.start;
                    }
                    if (criterion.end != null && valueItems.containsProperty("name", "end")) {
                        valueItems.find("name", "end").defaultValue = criterion.end;
                    }
                }
                if (valueItems) items.addList(valueItems);
            }
        } else {
            operatorItem.disabled = true;
        }

        if (this.showRemoveButton) {
            this.addAutoChild("removeButton", { prompt: this.removeButtonPrompt });
        }
        this.addMember(this.removeButton);


        this.addAutoChild("clause", {
            sortFields: this.sortFields,
            flattenItems: this.flattenItems,
            _suppressColWidthWarnings: true,
            items: items,
            height: 10,
            width: "100%",
            numCols: 5,
            colWidths: [100, this.operatorPickerWidth, "*", 10, "*"]
        });

        this.addMember(this.clause);

        this.fieldPicker = this.clause.getItem("fieldName");

        this.operatorPicker = this.clause.getItem("operator");
    }

    this.addMember(this.topOperatorForm, 0);
},

updateInlineTopOperator : function () {
    if (this.topOperatorAppearance != "inline") return;
    var operatorIndex = this.creator.showSelectionCheckbox ? 1 : 0;
    if (this.creator.isFirstClause(this)) {
        // Hide the operator
        this.topOperatorForm.items[operatorIndex].hide();
    } else {
        this.topOperatorForm.items[operatorIndex].show();
    }
},

// getEditorType for some field / operator. Call the same-named method on the FilterBuilder by default
// otherwise back-off to looking at the clause (a SearchForm subclass)'s implementation.

getEditorType : function (field, operatorID) {
    if (isc.isA.FilterBuilder(this.filterBuilder)) {
        return this.filterBuilder.getEditorType(field, operatorID);
    } else {
        return isc.FilterBuilder.getDefaultEditorType(field, this.getPrimaryDS(), operatorID);
    }
},

// create the form items that constitute the clause, based on the DataSource field involved and
// the chosen operator.
// Note: In the case where we are linked to multiple dataSources fieldName can differ from
// field.name (due to the dataSource ID prefix)
buildValueItemList : function (field, operator, fieldName) {
    // Sanity check only - we don't expect the operator to be unset but if it is log a warning
    if (operator == null) this.logWarn("buildValueItemList passed null operator");


    var editorType = this.getEditorType(field, operator.ID);

    var valueType = operator ? operator.valueType : "text",
        fieldType,
        baseFieldType,
        items = [],
        props
    ;


    if (field == null) {
        fieldType = "text";
    } else {
        if (fieldName == null) fieldName = field.name;
        fieldType = field.type;
    }

    baseFieldType = isc.SimpleType.getType(fieldType) || isc.SimpleType.getType("text");

    var isDateField = isc.SimpleType.inheritsFrom(fieldType, "date") ||
            isc.SimpleType.inheritsFrom(fieldType, "datetime");

    // We're going to work with *atomic* values - so if this is a SimpleType with a custom
    // storage paradigm that uses getAtomicValue() / setAtomicValue(), find its parent type
    // (The atomic type)
    while (baseFieldType.getAtomicValue != null) {
        var parentType = baseFieldType.inheritsFrom || "text";
        baseFieldType = isc.SimpleType.getType(parentType);
    }

    // We're not interested in the type object, just the name of the type
    baseFieldType = baseFieldType.name;



    if (field) {
        var dupField = {_origField:field};
        isc.addProperties(dupField, field);
        dupField.canEdit = field.canFilter;
        field = dupField;
        if (field.userFormula || field.userSummary) {
            field.canFilter = true;
            field.canEdit = true;
        }
    }

    editorType = this.getEditorType(field, operator.ID);

    // The previous call will return "staticText" for fields with canFilter:false, but that
    // isn't what we want if excludeNonFilterableFields is false - it leads to the field being
    // included in the clause with no way to set a value for it
    if (editorType == "staticText" && !this.excludeNonFilterableFields) editorType = "text";

    // a value of the same type as the field
    if (valueType == "fieldType" || valueType == "custom" || valueType == "valueSet")  {
        var dataType = baseFieldType;

        var valueFieldProps = this.getValueFieldProperties(fieldType, fieldName, operator.ID, "value");
        if (valueFieldProps && valueFieldProps.editorType) {
            // if getValueFieldProperties() specifies an editorType, respect it
            editorType = valueFieldProps.editorType;
        }

        var fieldDef = isc.addProperties(
            {
                type: dataType,
                // valueType - calling code can use this to determine whether the value type has
                // changed
                valueType:valueType,

                name: field ? field.name : null,
                showTitle: false,
                title : this.valueItemTitle,
                width: this.valueItemWidth,
                changed : function () {
                    this.form.creator.valueChanged(this, this.form);
                }
            },

            valueFieldProps
        );

        if (editorType) fieldDef.editorType = editorType;

        if (valueType == "valueSet") {
            fieldDef.multiple = true;
            fieldDef.showHintInField = true;
            fieldDef.hint = this.valueSetHint;
            fieldDef.parseEditorValue = function (value,form,item) {
                if (value == null) return value;
                var separator = item.multipleValueSeparator.trim();
                var re = new RegExp("[\\s]*[" + separator + "][\\s]*", "g");
                return value.split(re);
            };
            fieldDef.formatEditorValue = function (value, record, form, item) {
                if (value == null) return "";
                if (isc.isAn.Array(value)) {
                    return value.join(item.multipleValueSeparator);
                }
                return value;
            }
        }
        // Pick up DataSource presentation hints

        if (field != null && valueType != "custom") {
            fieldDef = this.combineFieldData(fieldDef, field);
        }

        fieldDef.name = "value";
        fieldDef.dataPath = null;

        if (fieldType == "enum") {
            fieldDef = isc.addProperties(fieldDef, {
                valueMap: field.valueMap
            });
        }

        if (baseFieldType == "boolean") {
            fieldDef = isc.addProperties(fieldDef, {
                showLabel: false,
                defaultValue: true,
                align: this.isRTL() ? "right" : "left"
            });
        }


        if (field && field.editorProperties) {
            if (field.editorType == "SelectItem" || field.editorType == "ComboBoxItem" ||
                field.editorType == "select" || field.editorType == "MultiComboBoxItem")
            {
                props = field.editorProperties;
                if (props.optionDataSource != null) fieldDef.optionDataSource = props.optionDataSource;
                if (props.valueField != null) fieldDef.valueField = props.valueField;
                if (props.displayField != null) fieldDef.displayField = props.displayField;
            } else {
                fieldDef = isc.addProperties({}, fieldDef, field.editorProperties);
            }
        }

        if (field.displayField != null || field.optionDataSource != null) {
            if (field.foreignKey) {
                // there's a foreignKey on the DSField - use that to assume values for ODS and
                // valueField, which are not available on DSField
                var key = field.foreignKey,
                    dotOffset = key.indexOf(".")
                ;
                if (!field.optionDataSource) fieldDef.optionDataSource = key.substring(0,dotOffset);
                if (!field.valueField) fieldDef.valueField = key.substring(dotOffset+1);
            } else {
                // there's no foreignKey on the DSField - just set the valueField to the passed
                // field's name
                if (!field.valueField) fieldDef.valueField = field.name;
            }
        }

        items.add(fieldDef);

    } else if (valueType == "fieldName") {
        // another field in the same DataSource
        // Note: This is a field picker, but its also the "value" field.
        // we therefore apply both valueFieldProperties and fieldPickerProperties to it, with
        // fieldPickerProperties winning if both are specified.


        props = isc.addProperties({}, this.fieldPickerDefaults, {
            showTitle: false,

            addUnknownValues: false,
            editorType: this.fieldDataSource ? "ComboBoxItem" : "SelectItem",
            valueType:valueType,
            width: this.valueItemWidth,
            textMatchStyle: this.fieldPicker.textMatchStyle,
            changed : function () {
                this.form.creator.valueChanged(this, this.form);
            }
        });
        if (this.sortFields) props.sortField = "value";
        if (this.fieldDataSource) {
            // using a fieldDataSource - apply this as the optionDataSource
            props = isc.addProperties(props, {
                completeOnTab: true,
                optionDataSource: this.fieldDataSource,
                valueField: "name",
                displayField: this.showFieldTitles ? "title" : "name",
                pickListProperties: { reusePickList : function () { return false; } }
            });
        } else {
            var altFieldNames = this.getFieldNames(true);
            altFieldNames.remove(fieldName);
            var fieldMap = {};
            for (var i = 0; i < altFieldNames.length; i++) {
                var nextFieldName = altFieldNames[i];
                if (this.showFieldTitles) {
                    var nextField  = this.getField(nextFieldName);
                    fieldMap[nextFieldName] = nextField.summaryTitle || nextField.title ||
                                              nextFieldName;
                } else {
                    fieldMap[nextFieldName] = nextFieldName;
                }
            }
            props = isc.addProperties(props, { valueMap: fieldMap });
        }
        items.add(
            isc.addProperties(props,
                this.getValueFieldProperties(fieldType, fieldName, operator.ID, "name"),
                this.fieldPickerProperties,
                // In SGWT the wrapper code picks up the 'name' attr on the field picker
                // override this here.
                {name: "value"})
        );

    } else if (valueType == "valueRange") {
        // two values of the same type as the field

        props = this.combineFieldData(
            isc.addProperties({
                type: baseFieldType, editorType:editorType, showTitle: false, width: this.valueItemWidth,
                changed : function () {
                    this.form.creator.valueChanged(this, this.form);
                }
            }), field);

        items.addList([
            isc.addProperties({}, props,
                { name: "start" },
                this.getValueFieldProperties(fieldType, fieldName, operator.ID, "start"),
                !isDateField ? {} : { rangePosition: "start" }
            ),
            isc.addProperties(
                { type: "staticText", name: "rangeSeparator", showTitle: false,
                    width: null, wrap: false, clipValue: false, clipStaticValue: false,
                    shouldSaveValue:false,
                    defaultValue: this.filterBuilder ? this.filterBuilder.rangeSeparator : null,
                    changed : function () {
                        this.form.creator.valueChanged(this, this.form);
                    }
                },
                this.getRangeSeparatorProperties(fieldType, fieldName, operator.ID)
            ),
            isc.addProperties({}, props,
                { name: "end" },
                this.getValueFieldProperties(fieldType, fieldName, operator.ID, "end"),
                !isDateField ? {} : { rangePosition: "end" }
            )
        ]);
    }

    if (this.validateOnChange) {
        for (var i = 0; i < items.length; i++) {
            isc.addProperties(items[i], {
                blur : function(form, item) {
                    if (!form.creator.itemsInError) form.creator.itemsInError = [];
                    if (!form.validate(null, null, true)) {
                        item.focusInItem();
                        if (!form.creator.itemsInError.contains(item)) {
                            form.creator.itemsInError.add(item);
                        }
                    } else {
                        if (form.creator.itemsInError.contains(item)) {
                            form.creator.itemsInError.remove(item);
                        }
                    }
                }
            });
        }
    }

    for (var i=0; i<items.length; i++) {
        if (items[i].showIf != null) delete items[i].showIf;
    }

    return items;
},

//> @type ValueItemType
// Enum used within the +link{FilterBuilder} class to indicate the role of a particular
// value-field form item within a filter clause.
//
// @value "value" This is the single form item that will populate the generated
//  +link{criterion.value} for this clause. This applies for operators with
//  +link{operator.valueType} of <code>"fieldType"</code> or <code>"custom"</code>.
// @value "name" This is the single form item that will populate the generated
//  +link{criterion.value} for +link{operator.valueType} of <code>"fieldName"</code>.
// @value "start" Indicates this item will generate the lower-bound value (or "start") when generating
//  criteria with +link{operator.valueType} <code>"valueRange"</code>.
// @value "end" Indicates this item will generate the higher-bound value (or "end") when generating
//  criteria with +link{operator.valueType} <code>"valueRange"</code>.
//
// @visibility external
//<

//> @method filterClause.getValueFieldProperties()
//
// Override to return properties for the FormItem(s) used for the "value" field displayed in this
// filterClause.
// <P>
// Default implementation simply calls +link{filterBuilder.getValueFieldProperties()} on the
// filterBuilder in which this clause is displayed.
// <P>
// Note that the +link{operator.valueType} impacts when this method is called. For operators with
// valueType <code>"fieldType"</code> or <code>"custom"</code>, a single value field is displayed.
// For operators with valueType <code>"valueRange"</code> two value-field items are displayed
// (one for the start and one for the end position). The <code>valueItemType</code> parameter may
// be used to determine which form item is being generated.
//
// @param type (FieldType) type of the DataSource field for this filter row
// @param fieldName (String) name of the DataSource field for this filter row
// @param operatorId (OperatorId) +link{OperatorId} for the chosen operator
// @param itemType (ValueItemType) What valueItem is being generated.
//
// @return (FormItem Properties) properties for the value field
// @visibility external
//<
getValueFieldProperties : function (type, fieldName, operatorId, itemType) {

    if (this.filterBuilder) {
        return this.filterBuilder.getValueFieldProperties(type,fieldName,operatorId,itemType);
    }
},


getRangeSeparatorProperties : function (type, fieldName, operatorId) {
    if (this.filterBuilder) return this.filterBuilder.getRangeSeparatorProperties(type,fieldName, operatorId);
},

//> @method filterClause.remove()
// Remove this clause by destroy()ing it.
//
// @visibility external
//<
remove : function () {
    this.markForDestroy();
},

getValues : function () {
    var clause = this.clause;

    return clause.getValues();
},

// ignoreDataPath option can be useful for display (used in the hilite-editor, for example)
getFieldName : function (ignoreDataPath) {
    if (this.fieldPicker) {

        var hasOptionDataSource = this.fieldPicker.optionDataSource != null;
        // if no optionDataSource it's a plain mapping of fieldName to titles so just use
        // getValue
        if (!hasOptionDataSource) return this.fieldPicker.getValue();
        // Otherwise each record in the optionDataSource represents a field -
        // try to grab the selected record and extract dataPath (if specified) or name
        var fieldRecord = this.fieldPicker.getSelectedRecord();
        if (fieldRecord) return ignoreDataPath ? fieldRecord.name :
                                    (fieldRecord.dataPath || fieldRecord.name);
        // No record - may just be due to the ODS data not having yet loaded - in this case
        // if we have a value it will have been explicitly set by the developer rather than
        // picked, so just use it (no need to worry about dataPath vs fieldname)
        var fieldValue = this.fieldPicker.getValue();
        if (fieldValue) return fieldValue;
    }
    return this.fieldName;
},

//> @method filterClause.getCriterion()
// Return the criterion specified by this FilterClause.
//
// @return (Criteria) The single criterion for this FilterClause
// @visibility external
//<
getCriterion : function (includeEmptyValues) {
    if (!this.clause) return null;

    var fieldName = this.getFieldName(),
        operator = this.operatorPicker ? this.operatorPicker.getValue() : null;

    if (!fieldName) return null;

    if (isc.isA.String(operator)) operator = this.getSearchOperator(operator, fieldName);

    if (operator == null) {
        return;
    }

    // getClauseValues - extracts the value / start & end attributes from the clause-form
    var criterion = this.getClauseValues(fieldName, operator);
    if (!includeEmptyValues) {
        // Ignore criteria where no value has been set, unless it is an operator (eg, isNull)
        // that does not require a value, or requires a start/end rather than a value, or
        // if the criterion is itself an AdvancedCriteria.
        // This behavior can be controlled by a parameter - to allow for the case where
        // you actually want to check for someField != null.
        if (!operator || (operator.valueType != "none" &&
            operator.valueType != "valueRange" && !isc.DS.isAdvancedCriteria(criterion) &&
            (criterion.value == null ||
            (isc.isA.String(criterion.value) && criterion.value == ""))))
        {
            return null;
        }
    }

    return criterion;
},

// getClauseValues() returns the values of the clause form
// - Contains fields for operator, fieldName, and either value or start/end by default
// - if operator.getCriterion is defined, use that rather than just returning values from the form.
// Note: we use filterClause in the RuleEditor where we're editing Rules (Validators).
// Because of this we make the various attribute names etc customizable -- for example
// a "contains" validator has a single value field but the attribute on the validator is
// validator.substring, not validator.value. It also uses 'getAttributesFromEditor' rather than
// getCriterion to handle custom UI.

customGetValuesFunction:"getCriterion",
valueAttribute:"value",
rangeStartAttribute:"start",
rangeEndAttribute:"end",
operatorAttribute:"operator",

getClauseValues : function (fieldName, operator) {

    var clause = this.clause,
        values = {},
        valueField = clause.getField("value"),
        startField = clause.getField("start"),
        endField = clause.getField("end"),

        valueAttribute = operator.valueAttribute || this.valueAttribute,
        rangeStartAttribute = operator.rangeStartAttribute || this.rangeStartAttribute,
        rangeEndAttribute = operator.rangeEndAttribute || this.rangeEndAttribute
    ;

    values[this.operatorAttribute] = clause.getValue("operator");


    if (fieldName != null) values.fieldName = fieldName;

    // If operator.getCriterion() [or validator.getAttributesFromEditor() for example] is
    // defined, call it.
    if (operator[this.customGetValuesFunction] &&
        isc.isA.Function(operator[this.customGetValuesFunction]))
    {
        if (valueField) {
            values = operator[this.customGetValuesFunction](fieldName, valueField);
        } else {

            var startCriterion = operator[this.customGetValuesFunction](fieldName, startField),
                endCriterion = operator[this.customGetValuesFunction](fieldName, endField);
            values.fieldName = startCriterion.fieldName;
            values[this.operatorAttribute] = startCriterion.operator;
            values[rangeStartAttribute] = startCriterion.value;
            values[rangeEndAttribute] = endCriterion.value;
        }
    } else {
        if (valueField) values[valueAttribute] = valueField.getValue();
        if (startField) values[rangeStartAttribute] = startField.getValue();
        if (endField) values[rangeEndAttribute] = endField.getValue();
    }

    // flag dates as logicalDates unless the field type inherits from datetime
    var field = this.getField(fieldName);
    if (isc.isA.Date(values[this.valueAttribute]) &&
        (!field || !isc.SimpleType.inheritsFrom(field.type, "datetime")))
    {
        values[valueAttribute].logicalDate = true;
    }
    return values;
},

// setClauseValues() - update the clause form items to reflect the object passed in

setCustomValuseFunction:"setCriterion",
setClauseValues : function (fieldName, operator, values) {
    if (this.clause == null) return;

    var clause = this.clause,
        valueField = clause.getField("value"),
        startField = clause.getField("start"),
        endField = clause.getField("end"),

        valueAttribute = operator.valueAttribute || this.valueAttribute,
        rangeStartAttribute = operator.rangeStartAttribute || this.rangeStartAttribute,
        rangeEndAttribute = operator.rangeEndAttribute || this.rangeEndAttribute;



    // Call the custom setter to update custom components if necessary.
    if (operator[this.customSetValuesFunction] &&
        isc.isA.Function(operator[this.customSetValuesFunction]))
    {
        if (valueField) {
            operator[this.customSetValuesFunction](fieldName, valueField, values);
        } else {

            operator[this.customSetValuesFunction](fieldName, startField, values);
            operator[this.customSetValuesFunction](fieldName, endField, values);
        }
    // no custom setter - just update the value field or range start/end
    } else {
        if (valueField) valueField.setValue(values[valueAttribute]);
        if (startField) startField.setValue(values[rangeStartAttribute]);
        if (endField) endField.setValue(values[rangeEndAttribute]);
    }
},

setDefaultFocus : function () {
    if (!this.clause) return;
    if (isc.isA.Function(this.clause.focusInItem)) this.clause.focusInItem("fieldName");
},

//> @method filterClause.validate
// Validate this clause.
// @return (Boolean) true if if the clause is valid, false otherwise
// @visibility external
//<
validate : function () {
    return this.clause ? this.clause.validate(null, null, true) : true;
},

itemChanged : function () {
    if (this.creator && isc.isA.Function(this.creator.itemChanged)) this.creator.itemChanged();
},

valueChanged : function (valueField, form) {
},

fieldNameChanged : function () {
    var enableItem = this.clause.getValue("fieldName") != null &&
                        this.clause.getValue("fieldName") != "";
    this.clause.getItem("operator").disabled = !enableItem;
    this.updateFields();
},

removeValueFields : function (typeChanged) {
    if (!this.clause) return;
    this.removeValueField("value", typeChanged);
    this.removeValueField("rangeSeparator", typeChanged);
    this.removeValueField("start", typeChanged);
    this.removeValueField("end", typeChanged);
},

removeValueField : function (fieldName, typeChanged) {
    var form = this.clause;
    if (form.getItem(fieldName)) {
        form.removeItem(fieldName);
        if (typeChanged) form.clearValue(fieldName);
    }
},

operatorChanged : function () {
    if (!this.clause) return;

    var form = this.clause,
        fieldName = this.fieldName || form.getValue("fieldName")
    ;

    if (fieldName == null) return;

    var field = this.getField(fieldName);
    var operator = this.getSearchOperator(form.getValue("operator"), field);

    this.updateValueItems(field,operator,fieldName);
},

// Note: In the case where we are linked to multiple dataSources fieldName can differ from
// field.name (due to the dataSource ID prefix)
updateValueItems : function (field,operator,fieldName) {

    var form = this.clause;
    var oldValueItem = form.getItem("value");
    var oldValueType = oldValueItem ? oldValueItem.valueType : null;

    this.removeValueFields();

    var items = this.buildValueItemList(field, operator, fieldName)


    form.addItems(items);
    var valueItem = form.getItem("value");
    if (valueItem &&
        // type changed (so was a field picker, now a text field, etc)
        ((valueItem.valueType != oldValueType) ||
         (valueItem.getValueMap() && valueItem._valueInValueMap &&
                 !valueItem._valueInValueMap(valueItem.getValue()) ||
         valueItem.optionDataSource ||
         !this.retainValuesAcrossFields))
        )
    {
        valueItem.clearValue();
    }
},

// updateFields() Fired when the user changes the fieldName field of this clause.
// Opportunity to determine the newly selected field type and update the operator valueMap and
// appropriate valueFields.
updateFields : function () {
    if (!this.clause) return;

    var form = this.clause,
        oldFieldName = this._lastFieldName,
        fieldName = this.fieldName || form.getValue("fieldName")
    ;
    if (fieldName == null) return;
    if (fieldName == oldFieldName) return;

    var field = this.getField(fieldName),
        oldField = this.getField(oldFieldName);

    if (!field) return;

    // note this setValueMap() call means if an operator was already chosen, it will be
    // preserved unless no longer valid for the new field
    form.getItem("operator").setValueMap(
        this.getFieldOperatorMap(field, false, "criteria", true)
    );

    var operator = form.getValue("operator");

    if (operator == null || form.getValue("operator") != operator) {
        // if the operator was lost from the valueMap, the value will have been cleared
        // Reset to the first option

        if (form.getValue("operator") == null) {
            form.getItem("operator").setValue(form.getItem("operator").getFirstOptionValue());
        }
        operator = form.getValue("operator");
    }

    // Now we've got the operator type we want, normalize it to a config object
    operator = this.getSearchOperator(operator, field);

    var typeChanged;
    if (form.getItem("value")) {
        var currentType = form.getItem("value").type,
            newType = field.type || "text";
        typeChanged = (currentType != newType);
    }

    // otherwise rebuild the value fields
    this.removeValueFields(typeChanged);
    form.addItems(this.buildValueItemList(field, operator, fieldName));

    // Clear out the currently entered value if
    //    1) the valueField data type has changed
    //    2) the new valueField has a valueMap and the current value doesn't appear in it
    //    3) either the old or new field has a valueMap or optionDataSource
    //    4) this.retainValuesAcrossFields is false
    var valueItem = form.getItem("value");
    if (!typeChanged) {
        var shouldClear = (
            (field.valueMap || field.optionDataSource) ||
            (oldField && (oldField.valueMap || oldField.optionDataSource)) ||
            !this.retainValuesAcrossFields
        );

        if (valueItem && shouldClear) valueItem.clearValue();
    }

    // validate the fields to update the error-icon
    if (valueItem) valueItem.validate();
    // For now always clear out range fields - also re-validate them
    if (form.getItem("start")) {
        form.setValue("start", null);
        form.getItem("start").validate();
    }
    if (form.getItem("end")) {
        form.setValue("end", null);
        form.getItem("end").validate();
    }


    this._lastFieldName = fieldName;
},

//> @method filterClause.getFieldOperators()
// Get the list of +link{OperatorId, operatorIds} that are valid for this field.  By default,
// calls through to the same method on +link{filterBuilder.getFieldOperators, filterBuilder},
// which defaults to all operators returned by +link{dataSource.getFieldOperators()}.
// <P>
// Called whenever the fieldName is changed.
//
// @param fieldName (String) the name of the field for which to return the set of available
//           operators
// @return (Array of OperatorId) valid operators for this field
// @visibility external
//<
getFieldOperators : function (fieldName) {
    var field = this.getField(fieldName);
    var filterBuilder = this.getFilterBuilder();
    return filterBuilder && filterBuilder.getFieldOperators(fieldName, field);
},

// called when the user changes the topOperator via a form - inline only.  We only implement
// it so we can use the same form config for builder-level and clause-level topOperators
topOperatorChanged : function (newOp) {
}

});



isc.FilterClause.registerStringMethods({
    remove : ""
});


//> @class FilterBuilder
// A form that allows the user to input advanced search criteria, including operators on
// field values such as "less than", and sub-clauses using "AND" and "OR" operators.
// <P>
// A FilterBuilder produces an +link{AdvancedCriteria} object, which the +link{DataSource}
// subsystem can use to filter datasets, including the ability to perform such filtering within
// the browser for datasets that are completely loaded.
// <P>
// The operators available for each field can be customized at the DataSource level via
// +link{DataSourceField.validOperators()}, +link{DataSource.setTypeOperators()} and related
// APIs.
//
// @treeLocation Client Reference/Forms
// @visibility external
//<
isc.defineClass("FilterBuilder", "Layout");

isc.FilterBuilder.addClassProperties({
//> @attr filterBuilder.missingFieldPrompt (String: "[missing field definition]" : IR)
// The message to display next to fieldNames that do not exist in the available dataSource.
// @group i18nMessages
// @visibility external
//<
missingFieldPrompt: "[missing field definition]"

});

isc.FilterBuilder.addClassMethods({


//> @classMethod filterBuilder.getFilterDescription()
// @include DataSource.getAdvancedCriteriaDescription
// @visibility external
//<

getFilterDescription : function (criteria, dataSource) {
    return isc.DataSource.getAdvancedCriteriaDescription(criteria, dataSource);
}

});

isc.FilterBuilder.addProperties({

// Layout: be a minimum height stack by default
// ---------------------------------------------------------------------------------------
vertical:false,
vPolicy:"none",
height:1,
defaultWidth:400,

//> @attr filterBuilder.fieldDataSource (DataSource : null : IR)
// If specified, the FilterBuilder will dynamically fetch DataSourceField definitions from
// this DataSource rather than using +link{filterBuilder.dataSource}.  The +link{fieldPicker}
// will default to being a +link{ComboBoxItem} rather than a +link{SelectItem} so that the user
// will have type-ahead auto-completion.
// <P>
// The records returned from the <code>fieldDataSource</code> must have properties
// corresponding to a +link{DataSourceField} definition, at a minimum,
// +link{DataSourceField.name,"name"} and +link{DataSourceField.type,"type"}.  Any property
// legal on a DataSourceField is legal on the returned records, including
// +link{DataSourceField.valueMap,valueMap}.
// <P>
// Even when a <code>fieldDataSource</code> is specified, +link{filterBuilder.dataSource} may
// still be specified in order to control the list of
// +link{DataSource.setTypeOperators,valid operators} for each field.
//
// @visibility external
//<

//> @attr filterBuilder.sortFields (Boolean : true : IR)
// Should the +link{fieldPicker} items be sorted alphabetically in the drop down list.
// @visibility external
//<
sortFields:true,

//> @attr filterBuilder.fieldPicker (MultiAutoChild PickList : null : IR)
// AutoChild for the +link{FormItem} that allows a user to pick a DataSource field when
// creating filter clauses.
// <P>
// This will be a +link{SelectItem} by default, or a +link{ComboBoxItem} if
// +link{filterBuilder.fieldDataSource} has been specified.
//
// @visibility external
//<

fieldPickerDefaults: {
    type: "SelectItem",
    name: "fieldName",
    textMatchStyle: "startsWith",
    showTitle: false,
    changed : function () { this.form.creator.fieldNameChanged(this.form); }
},

//> @attr filterBuilder.fieldPickerTitle (String : "Field Name" : IR)
// The title for the +link{filterBuilder.fieldPicker, field-picker} select-item.
// @group i18nMessages
// @visibility external
//<
fieldPickerTitle: "Field Name",

//> @attr filterBuilder.fieldPickerProperties (FormItem Properties : null : IR)
// Properties to combine with the +link{fieldPicker} autoChild FormItem.
//
// @visibility external
//<

//> @attr filterBuilder.operatorPicker (MultiAutoChild SelectItem : null : IR)
// AutoChild for the +link{FormItem} that allows a user to select the operator
// when creating filter clauses. Each clause will create an operatorPicker automatically.
// To customize this item, use +link{operatorPickerProperties}
//
// @visibility external
//<

//> @attr filterBuilder.operatorPickerProperties (FormItem Properties : null : IR)
// Properties to combine with the +link{operatorPicker} autoChild FormItem.
//
// @visibility external
//<
operatorPickerDefaults : {
    // list of operators
    name:"operator",
    type:"select",
    showTitle:false,
    // don't allow addUnknownValues - it's a fixed list applicable to the selected field
    addUnknownValues:false,
    defaultToFirstOption:true,
    changed : function () { this.form.creator.operatorChanged(this.form); }
},

//> @attr filterBuilder.operatorPickerTitle (String : "Operator" : IR)
// The title for the operator-picker select-item.
// @group i18nMessages
// @visibility external
//<
operatorPickerTitle: "Operator",

//> @attr filterBuilder.fieldPickerWidth (Integer | String : "*" : IR)
// Width for the field picker formItem displayed in clauses within this FilterBuilder.
// @visibility external
//<
fieldPickerWidth: "*",

//> @attr filterBuilder.operatorPickerWidth (Integer | String : 150 : IR)
// Width for the operator picker formItem displayed in clauses within this FilterBuilder.
// @visibility external
//<
operatorPickerWidth: 150,

//> @attr filterBuilder.valueItemWidth (Integer | String : 150 : IR)
// Width for the value-chooser formItem displayed in clauses within this FilterBuilder.
// Note that depending on the selected operator type, this item may not be displayed, or
// may have different characteristics. See +link{getValueFieldProperties()} for information
// on customizing the value item.
//
// @visibility external
//<
valueItemWidth: 150,



// Schema and operators
// ---------------------------------------------------------------------------------------

//> @attr filterBuilder.dataSource (DataSource or ID : null : IRW)
// DataSource this filter should use for field definitions and available +link{Operator}s.
// @visibility external
//<
setDataSource : function(ds) {
    var aDS = isc.DataSource.get(ds);
    if (!this.dataSource || (isc.DataSource.get(this.dataSource).ID != aDS.ID)) {
        this.dataSource = aDS;
        if (this.clauses) this.clearCriteria();
        else this.rebuild();
    }
},


setDataSources : function (dsList) {
    this.dataSources = dsList;
    if (this.clauses) this.clearCriteria();
    else this.rebuild();
},

//> @attr filterBuilder.criteria (AdvancedCriteria : null : IRW)
// Initial criteria.
// <P>
// When initialized with criteria, appropriate clauses for editing the provided criteria will
// be automatically generated.
// <P>
// Note that empty or partial criteria are allowed, for example, criteria that specify
// +link{criterion.fieldName} only will generate an expression with the operator not chosen
// yet, and a +link{criterion} with a logical operator ("and" or "or") but not
// +link{criterion.criteria,subcriteria} defined will generate an empty subclause.
// @visibility external
//<

//> @attr filterBuilder.saveOnEnter (boolean : null : IR)
// If true, when the user hits the Enter key while focused in a text-item in this
// FilterBuilder, we automatically invoke the user-supplied +link{filterBuilder.search()} method.
// @visibility external
//<

//> @attr filterBuilder.showFieldTitles (Boolean : true : IR)
// If true (the default), show field titles in the drop-down box used to select a field for querying.
// If false, show actual field names instead.
// @visibility external
//<
showFieldTitles: true,

//> @attr filterBuilder.validateOnChange (Boolean : true : IR)
// If true (the default), validates each entered value when it changes, to make sure it is a
// a valid value of its type (valid string, number, and so on).  No other validation is
// carried out.  If you switch this property off, it is still possible to validate the
// <code>FilterBuilder</code> by calling +link{filterBuilder.validate()} from your own code.
// @visibility external
//<
validateOnChange: true,

// Add/remove buttons
// ---------------------------------------------------------------------------------------

//> @attr filterBuilder.showRemoveButton (Boolean : true : IR)
// If set, a button will be shown for each clause allowing it to be removed.
// @visibility external
//<
showRemoveButton:true,

//> @attr filterBuilder.removeButtonPrompt (string : "Remove" : IR)
// The hover prompt text for the remove button.
//
// @group i18nMessages
// @visibility external
//<
removeButtonPrompt: "Remove",

//> @attr filterBuilder.removeButton (AutoChild ImgButton : null : IR)
// The removal ImgButton that appears before each clause if
// +link{showRemoveButton} is set.
// @visibility external
//<
removeButtonDefaults : {
    _constructor:isc.ImgButton,
    width:18, height:18, layoutAlign:"center",
    src:"[SKIN]/actions/remove.png",
    showRollOver:false, showDown:false,
    showDisabled:false, // XXX
    //prompt:"Remove",
    click: function () { this.creator.removeButtonClick(this.clause); }
},

//> @attr filterBuilder.showAddButton (Boolean : true : IR)
// If set, a button will be shown underneath all current clauses allowing a new clause to be
// added.
// @visibility external
//<
showAddButton:true,

//> @attr filterBuilder.addButtonPrompt (string : "Add" : IR)
// The hover prompt text for the add button.
//
// @group i18nMessages
// @visibility external
//<
addButtonPrompt: "Add",

//> @attr filterBuilder.addButton (AutoChild ImgButton : null : IR)
// An ImgButton that allows new clauses to be added if +link{showAddButton}
// is set.
// @visibility external
//<
addButtonDefaults : {
    _constructor:isc.ImgButton,
    autoParent:"buttonBar",
    width:18, height:18,
    src:"[SKIN]/actions/add.png",
    showRollOver:false, showDown:false,
    //prompt:"Add",
    click: function () { this.creator.addButtonClick(this.clause); }
},

buttonBarDefaults : {
    _constructor:isc.HStack,
    autoParent:"clauseStack",
    membersMargin:4,
    defaultLayoutAlign:"center",
    height:1
},

addButtonClick : function () {
    this.addNewClause();
},

removeButtonClick : function (clause) {
    if (!clause) return;
    this.removeClause(clause);
},

//> @method filterBuilder.removeClause()
// Remove a clause this FilterBuilder is currently showing.
// @param clause (FilterClause) clause as retrieved from filterBuilder.clauses
// @visibility external
//<
removeClause : function (clause) {
    // remove the clause from the clauses array and destroy it
    this.clauses.remove(clause);
    if (this.clauseStack) this.clauseStack.hideMember(clause, function () { clause.destroy(); });
    // update the first removeButton
    this.updateFirstRemoveButton();
    if (this.clauses[0] && this.clauses[0].updateInlineTopOperator) this.clauses[0].updateInlineTopOperator();
    clause.filterBuilder = null;
    // fire filterChanged
    if (isc.isA.Function(this.filterChanged)) this.filterChanged();
},

//> @attr filterBuilder.allowEmpty (Boolean : false : IR)
// If set to false, the last clause cannot be removed.
// @visibility external
//<

updateFirstRemoveButton : function () {
    var firstClause = this.clauses[0];

    if (!firstClause || !firstClause.removeButton) return;

    if (this.clauses.length == 1 && !this.allowEmpty) {
        firstClause.removeButton.disable();
        firstClause.removeButton.setOpacity(50); // XXX need media with disabled state
    } else if (this.clauses.length > 1) {
        firstClause.removeButton.enable();
        firstClause.removeButton.setOpacity(100); // XXX need media with disabled state
    }
},

isFirstClause : function (clause) {
    return this.clauses[0] == clause;
},

// Top-level Operator
// ---------------------------------------------------------------------------------------

//> @type LogicalOperator
// Operators that can evaluate a set of criteria and produce a combined result.
//
// @value "and" true if all criteria are true
// @value "or" true if any criteria are true
// @value "not" true if all criteria are false
// @visibility external
//<

//> @attr filterBuilder.retainValuesAcrossFields (Boolean : true : IRW)
// Dictates whether values entered by a user should be retained in the value fields when a
// different field is selected.  Default value is true.
// <P>
// Note that, when switching between fields that have an optionDataSource or valueMap, this
// property is ignored and the values are never retained.
// @visibility external
//<
retainValuesAcrossFields: true,


//> @attr filterBuilder.topOperatorOptions (Array of OperatorId : ["and", "or", "not"] : IR)
// Logical operators to allow for +link{topOperatorAppearance}s of "radio" and "bracket".
// <P> Note that this list may be further limited according to the
// +link{DataSource.getTypeOperatorMap, available operators} returned by the
// +link{class:DataSource}.
//
// @visibility external
//<
topOperatorOptions: ["and", "or", "not"],

//> @attr filterBuilder.topOperator (LogicalOperator : "and" : IRW)
// Default logical operator for all top-level clauses in the FilterBuilder.
// <P>
// May be able to be changed by the user via the UI, according to +link{topOperatorAppearance}.
// @visibility external
//<
topOperator: "and",

//> @attr filterBuilder.radioOptions (Array of OperatorId : ["and", "or", "not"] : IR)
// Logical operators to allow if we have a +link{topOperatorAppearance} of "radio".
//
// @visibility external
// @deprecated in favor of +link{topOperatorOptions}.
//<
//radioOptions: ["and", "or", "not"],

//> @method filterBuilder.setTopOperator()
// Programmatically change the +link{topOperator} for this FilterBuilder.
// @param operator (OperatiorId) new top-level operator
// @visibility external
//<
setTopOperator : function (newOp) {
    this.topOperator = newOp;
    var appearance = this.topOperatorAppearance;

    if (appearance == "bracket") {
        this.topOperatorForm.setValue("operator", newOp);
    } else if (appearance == "radio") {
        this.radioOperatorForm.setValue("operator", newOp);
    }
},

// called when the user changes the topOperator via a form
topOperatorChanged : function (newOp) {
    this.topOperator = newOp;
    if (isc.isA.Function(this.filterChanged)) this.filterChanged();

},

//> @type TopOperatorAppearance
// Interface to use for showing and editing the +link{filterBuilder.topOperator,top-level operator}
// of a FilterBuilder.
//
// @value "radio" radio buttons appear at the top of the form
//
// @value "bracket" a SelectItem appears with a "bracket" spanning all top-level clauses,
// exactly the same appearance used for showing
// +link{filterBuilder.showSubClauseButton,subClauses}, if enabled.
//
// @value "inline" each line in the FilterBuilder is a top-level item, with a SelectItem shown
// on the left that allows the user to choose between the main operator in force (either "and"
// or "or", depending on the setting of topOperator) and "and not".
//
// @value "none" no interface is shown.  The top-level operator is expected to be shown to
// the user outside the FilterBuilder, and, if editable, +link{filterBuilder.setTopOperator()}
// should be called to update it
// @visibility external
//<

//> @attr filterBuilder.topOperatorAppearance (TopOperatorAppearance : "bracket" : IRW)
// How to display and edit the +link{topOperator,top-level operator} for this FilterBuilder.
// <P>
// See +link{type:TopOperatorAppearance} for a list of options.
// @visibility external
//<
topOperatorAppearance:"bracket",

//> @method filterBuilder.setTopOperatorAppearance()
// Modify +link{topOperatorAppearance} at runtime.
// <P>
// Note that when changing from "bracket" to "radio" mode the criteria
// will be flattened by calling +link{DataSource.flattenCriteria} which could
// result in a logical change to the criteria.
//
// @param (TopOperatorAppearance) new topOperatorAppearance
// @group  formTitles
// @visibility external
// @example formLayoutTitles
//<
setTopOperatorAppearance : function (appearance) {
    if (this.topOperatorAppearance == appearance) return;
    // Keep current criteria for new form
    var criteria = this.getCriteria(true);

    if (this.topOperatorAppearance == "bracket" && appearance == "radio") {
        criteria = isc.DataSource.flattenCriteria(criteria);
    }
    this.topOperatorAppearance = appearance;

    this._recreateForm(criteria);
},

//> @attr filterBuilder.radioOperatorLayout (AutoChild HLayout : null : IR)
// HLayout of radioOperationForm and optional modeSwitcher.
// @visibility external
//<
radioOperatorLayoutDefaults : {
    autoParent:"clauseStack",
    _constructor:isc.HLayout,
    height:1,
    width:1
},

//> @attr filterBuilder.radioOperatorForm (AutoChild DynamicForm : null : IR)
// With +link{topOperatorAppearance}:"radio", form that appears above the stack of clauses
// and allows picking the +link{LogicalOperator} for the overall FilterBuilder.
// <P>
// By default, consists of a simple RadioGroupItem.
// @visibility external
//<
radioOperatorFormDefaults : {
    autoParent:"radioOperatorLayout",
    height:1, numCols:1, colWidths:["*"], width:275,
    items : [
        { name:"operator", type:"radioGroup",
          showTitle:false, title:"Overall Operator",
          vertical:false,
          changed : function (form, item, value) {
              form.creator.topOperatorChanged(value);
          }
        }
    ]
},
radioOperatorFormConstructor: isc.DynamicForm,

//> @attr filterBuilder.modeSwitcher (AutoChild Label : null : IR)
// Label to change between simple and advanced mode. When clicked the filter mode
// is switched to the other mode. This label is only shown if
// +link{showModeSwitcher, showModeSwitcher} is true.
// <P>
// Shows either +link{modeSwitcherSimpleMessage,modeSwitcherSimpleMessage} or
// +link{modeSwitcherAdvancedMessage,modeSwitcherAdvancedMessage}
// depending on the current state of the filter.
// @visibility external
//<
modeSwitcherDefaults : {
    height:30,
    autoFit:true,
    wrap:false,
    cursor:"pointer",
    click : function () {
        this.creator.switchMode();
    }
},
modeSwitcherConstructor: isc.Label,

//> @attr filterBuilder.radioOperatorTitle (String : "Overall Operator" : IR)
// The title for the Operator RadioGroupItem displayed in the +link{radioOperatorForm}.
// @group i18nMessages
// @visibility external
//<
radioOperatorTitle: "Overall Operator",

//> @attr filterBuilder.showSelectionCheckbox (Boolean : false : IR)
// If true, causes a CheckboxItem to appear to the left of each clause in "inline"
// +link{topOperatorAppearance,appearance}.  This checkbox allows the user to select
// individual clauses so that, for example, clauses can be removed from the filterBuilder
// by application code.  This property is ignored for appearances other than "inline".
// @visibility external
//<

//> @attr filterBuilder.topOperatorForm (AutoChild DynamicForm : null : IR)
// With +link{topOperatorAppearance} "bracket" and "inline", a form that appears to the left
// of the stack of clauses and allows picking the +link{LogicalOperator} for the overall
// FilterBuilder (or for that specific FilterClause, in the case of "inline")
// <P>
// By default, consists of a CheckboxItem if +link{showSelectionCheckbox} is true, and a
// simple SelectItem containing the available logical operators.
// <P>
// If this FilterBuilder shows nested sub-clauses, the same defaults will be applied to the
// top-operator item for each sub-clause.
//
// @visibility external
//<
topOperatorFormDefaults : {
    height:1,
    width:80, numCols:1, colWidths:["*"],
    layoutAlign:"center",
    _constructor:isc.DynamicForm
},

//> @attr filterBuilder.topOperatorItem (AutoChild SelectItem : null : IR)
// Automatically generated SelectItem autoChild shown in the +link{topOperatorForm}.
// Developers may customize this item using the standard autoChild pattern (by
// modifying <code>topOperatorItemDefaults</code> and
// <code>topOperatorItemProperties</code>).
// <P>
// If this FilterBuilder shows nested sub-clauses, the same defaults will be applied to the
// top-operator item for each sub-clause.
//
// @visibility external
//<
topOperatorItemDefaults:{
    name:"operator",
    type: "select",
    showTitle:false,
    changed : function (form, item, value) {
        form.creator.topOperatorChanged(value);
    }
},

//> @attr filterBuilder.topOperatorItemWidth (Number | String : "*" : IR)
// Width for the +link{topOperatorItem} autoChild.
// @visibility external
//<
topOperatorItemWidth:"*",

//> @attr filterBuilder.topOperatorTitle (String : "Clause Operator" : IR)
// The title for the left-aligned Operator selectItem in the +link{topOperatorForm}.
// @group i18nMessages
// @visibility external
//<
topOperatorTitle: "Clause Operator",

//> @attr filterBuilder.defaultSubClauseOperator (LogicalOperator : "or" : IR)
// Default operator for subclauses added via the +link{subClauseButton}.
// @visibility external
//<
defaultSubClauseOperator:"or",

//> @attr FilterBuilder.matchAllTitle (String : "Match All" : IR)
// Title for the "Match All" (and) operator
// when using +link{topOperatorAppearance,topOperatorAppearance}:"radio".
// @group i18nMessages
// @visibility external
//<
matchAllTitle: "Match All",

//> @attr FilterBuilder.matchNoneTitle (String : "Match None" : IR)
// Title for the "Match None" (not) operator
// when using +link{topOperatorAppearance,topOperatorAppearance}:"radio".
// @group i18nMessages
// @visibility external
//<
matchNoneTitle: "Match None",

//> @attr FilterBuilder.matchAnyTitle (String : "Match Any" : IR)
// Title for the "Match Any" (or) operator
// when using +link{topOperatorAppearance,topOperatorAppearance}:"radio".
// @group i18nMessages
// @visibility external
//<
matchAnyTitle: "Match Any",

//> @attr FilterBuilder.inlineAndTitle (String : "and" : IR)
// Title for the "And" operator (only applicable to the "inline" appearance)
// @group i18nMessages
// @visibility external
//<
inlineAndTitle: "and",

//> @attr FilterBuilder.inlineOrTitle (String : "or" : IR)
// Title for the "Or" operator (only applicable to the "inline" appearance)
// @group i18nMessages
// @visibility external
//<
inlineOrTitle: "or",

//> @attr FilterBuilder.inlineAndNotTitle (String : "and not" : IR)
// Title for the "And Not" operator (only applicable to the "inline" appearance)
// @group i18nMessages
// @visibility external
//<
inlineAndNotTitle: "and not",

//> @attr FilterBuilder.modeSwitcherAdvancedMessage (String : "Advanced.." : IR)
//Title for the "Advanced.." mode switcher label (only applicable to the "radio" appearance).
//@group i18nMessages
//@visibility external
//<
modeSwitcherAdvancedMessage: "Advanced..",

//> @attr FilterBuilder.modeSwitcherSimpleMessage (String : "Simple Mode.." : IR)
// Title for the "Simple Mode.." mode switcher label (only applicable to the "bracket" appearance).
// @group i18nMessages
// @visibility external
//<
modeSwitcherSimpleMessage: "Simple Mode..",

//> @attr FilterBuilder.modeSwitcherFlattenWarningMessage (String : "Criteria will be modified to fit in simpler editing interface" : IR)
// Message displayed when switching to "radio" mode if the criteria will be logically changed.
// @group i18nMessages
// @visibility external
//<
modeSwitcherFlattenWarningMessage: "Criteria will be modified to fit in simpler editing interface",

//> @attr filterBuilder.showModeSwitcher (boolean : null : IR)
// When enabled allows FilterBuilder in <code>topOperatorAppearance:"radio"</code> or
// <code>topOperatorAppearance:"bracket"</code> mode to be switch to the other view by the user.
// "radio" mode is considered simple where "bracket" mode is advanced mode.
// <P>
// Note that when switching from "bracket" to "radio" mode any entered criteria will be
// flattened by calling +link{DataSource.flattenCriteria}. If the criteria cannot be
// flattened without losing symantics (see +link{DataSource.canFlattenCriteria}) the user is
// prompted to confirm.
//
// @see modeSwitcherSimpleMessage
// @see modeSwitcherAdvancedMessage
// @see modeSwitcherFlattenWarningMessage
//
// @visibility external
//<

// Init
// ---------------------------------------------------------------------------------------

// getPrimaryDS() - this returns the dataSource used to drive field operators etc.

getPrimaryDS : function (fieldName) {
    if (this.dataSources) {
        if (fieldName == null) {
            return isc.DataSource.get(this.dataSources[0]);
        }
        if (isc.isAn.Object(fieldName)) {
            if (fieldName._origField != null) {
                fieldName = fieldName._origField;
            }
            for (var i = 0; i < this.dataSources.length; i++) {
                var fields = isc.getValues(this.dataSources[i].getFields());
                if (fields.contains(fieldName)) return this.dataSources[i];
            }
            fieldName = fieldName.name;
        }
        return isc.DataSource.getDataSourceForField(fieldName, this.dataSources);
    }
    if (this.dataSource) return this.getDataSource();
    else if (this.fieldDataSource) return this.fieldDataSource;
},

getTopOperatorMap : function (type) {
    var ds = this.getPrimaryDS(),
        map = type == "bracket" ?
                ds ? ds.getTypeOperatorMap("text", true, "criteria") : null : null
    ;

    if (!map) {
        map =  {
                    "and": this.matchAllTitle,
                    "or": this.matchAnyTitle,
                    "not": this.matchNoneTitle
        };
    }
    var topOp = this.topOperator,
        options = this.topOperatorOptions,
        result = {}
    ;

    if (!options.contains(topOp)) this.topOperator = options[0];

    if (map) {
        for (var i = 0; i<options.length; i++) {
            if (map[options[i]]) result[options[i]] = map[options[i]];
        }
    }

    return result;
},

initWidget : function () {
    this.Super("initWidget", arguments);

    this.rebuild();
},
rebuild : function () {
    if (isc.isA.String(this.fieldDataSource))
        this.fieldDataSource = isc.DS.get(this.fieldDataSource);

    if (isc.isA.String(this.dataSource))
        this.dataSource = isc.DS.get(this.dataSource);

    if (!this.getPrimaryDS()) this.logWarn("No available DataSources.");

    if (this.fieldDataSource && this.criteria) this._initializingClauses = true;

    this.fieldPickerDefaults.title = this.fieldPickerTitle;


    if (this.radioOptions) this.topOperatorOptions = this.radioOptions.duplicate();
    delete this.radioOptions;

    // set strings for button defaults
    this.addButtonDefaults.prompt = this.addButtonPrompt;
    this.removeButtonDefaults.prompt = this.removeButtonPrompt;
    this.subClauseButtonDefaults.prompt = this.subClauseButtonPrompt;
    this.subClauseButtonDefaults.title = this.subClauseButtonTitle;

    this._createForm();
},
_recreateForm : function (criteria) {
    this.criteria = criteria;

    // Reset form so it can be completed recreated
    delete this.showSubClauseButton;
    this.topOperatorForm = this.bracket = this.clauseStack = null;
    this.radioOperatorForm = this.radioOperatorLayout = this.modeSwitcher = null;
    this.buttonBar = this.addButton = this.subClauseButton = null;

    for (var i = 0, clauses = this.clauses; i < clauses.length; i++) {
        clauses[i].destroy();
    }
    var members = this.getMembers().duplicate();
    for (var i = 0; i < members.length; i++) {
        this.removeMember(members[i]);
        members[i].destroy();
    }

    this._createForm();
},
_createForm : function () {
    var undef;
    if (this.showSubClauseButton == undef) {
        this.showSubClauseButton = (this.topOperatorAppearance != "radio" &&
                                    this.topOperatorAppearance != "inline");
    }

    this.clauses = [];

    var topOp = this.topOperatorAppearance;


    var ds = this.getPrimaryDS(),
        tempMap = this.getTopOperatorMap(topOp)
    ;

    if (this.showModeSwitcher) {
        var contents = "<span style='color:blue;text-decoration:underline;'>" +
            (topOp == "radio" ? this.modeSwitcherAdvancedMessage : this.modeSwitcherSimpleMessage) +
            "</span>";
        this.modeSwitcher = this.createAutoChild("modeSwitcher", { contents:contents });
    }

    if (topOp == "bracket") {
        if (this.showTopRemoveButton) {
            // When the FilterBuilder is being used as a subclause it needs a remove button.
            // Our parent FilterBuilder could tack one on, but only by introducing an extra
            // layer of nesting, so we manage it here.
            var removeButton = this.removeButton = this.createAutoChild("removeButton", {
                click : function () {
                    this.creator.parentClause.removeButtonClick(this.creator);
                }
            });
            this.addMember(removeButton);
        }
        var opFormConfig = {
            items:[isc.addProperties(
                    {width:this.topOperatorItemWidth},
                     this.topOperatorItemDefaults, this.topOperatorItemProperties
                   )]
        };
        this.addAutoChild("topOperatorForm", opFormConfig);

        this.topOperatorForm.items[0].title = this.topOperatorTitle;
        this.topOperatorForm.items[0].valueMap = tempMap;
        this.topOperatorForm.items[0].defaultValue = this.topOperator;

        this.addAutoChild("bracket");
    }
    this.addAutoChild("clauseStack");
    this.clauseStack.hide();
    if (topOp == "radio") {
        this.addAutoChild("radioOperatorLayout");
        this.addAutoChild("radioOperatorForm");
        var radioMap = tempMap;

        this.radioOperatorForm.items[0].title = this.radioOperatorTitle;
        this.radioOperatorForm.items[0].valueMap = radioMap;
        this.radioOperatorForm.items[0].defaultValue = this.topOperator;
        if (this.showModeSwitcher) this.radioOperatorLayout.addMember(this.modeSwitcher);
    }
    this.addAutoChildren(["buttonBar", "addButton", "subClauseButton"]);
    if (this.showModeSwitcher && topOp == "bracket") {
        this.buttonBar.addMember(isc.LayoutSpacer.create({ width:20 }));
        this.buttonBar.addMember(this.modeSwitcher);
    }

    // support criteria being passed with null elements
    this.stripNullCriteria(this.criteria);
    this.setCriteria(this.criteria);

},


//> @attr filterBuilder.clauseStack (AutoChild VStack : null : IR)
// VStack of all clauses that are part of this FilterBuilder
// @visibility external
//<
clauseStackDefaults : {
    _constructor:isc.VStack,
    height:1,
    membersMargin:1, // otherwise brackets on subclauses are flush
    animateMembers: true,
    animateMemberTime: 150
},

// Switch between simple and advanced mode (radio and bracket)
switchMode : function () {
    if (this.topOperatorAppearance == "bracket") {
        var criteria = this.getCriteria(true);
        if (!isc.DataSource.canFlattenCriteria(criteria)) {
            var _this = this;
            isc.confirm(this.modeSwitcherFlattenWarningMessage, function(value) {
                if (value) _this.setTopOperatorAppearance("radio");
            });
            return;
        }
        this.setTopOperatorAppearance("radio");
    } else {
        this.setTopOperatorAppearance("bracket");
    }
},

// Clause creation
// ---------------------------------------------------------------------------------------

clauseConstructor: "FilterClause",


addNewClause : function (criterion, field, negated) {

    var filterClause = this.createAutoChild("clause",
      isc.addProperties({}, this.inheritedClauseProperties,
      {
        visibility: "hidden",
        flattenItems: true,
        criterion: criterion,

        // Various ways we can define dataSources/field configs...
        dataSources: this.dataSources,
        dataSource: this.dataSource,
        fieldDataSource: this.fieldDataSource,
        // copy sortFields onto the 'clause' so buildValueItemList can
        // apply the appropriate defaults to the pickList
        sortFields:this.sortFields,

        validateOnChange: this.validateOnChange,
        showFieldTitles: this.showFieldTitles,
        showRemoveButton: this.showRemoveButton,
        removeButtonPrompt: this.removeButtonPrompt,
        retainValuesAcrossFields: this.retainValuesAcrossFields,
        field: field,
        fieldData: this.fieldData,
        fieldPickerDefaults: this.fieldPickerDefaults,
        fieldPickerProperties: this.fieldPickerProperties,

        fieldPickerWidth: this.fieldPickerWidth,
        operatorPickerWidth: this.operatorPickerWidth,
        valueItemWidth: this.valueItemWidth,

        operatorPickerDefaults: this.operatorPickerDefaults,
        operatorPickerProperties: this.operatorPickerProperties,
        remove : function () {
            this.creator.removeClause(this);
        },
        fieldNameChanged : function () {
            this.Super("fieldNameChanged", arguments);
            this.creator.fieldNameChanged(this);
        },
        topOperatorAppearance: this.topOperatorAppearance,
        topOperator: this.topOperator,
        topOperatorOptions: this.topOperatorOptions,

        topOperatorFormDefaults: this.topOperatorFormDefaults,
        topOperatorFormProperties: this.topOperatorFormProperties,
        topOperatorItemDefaults: this.topOperatorItemDefaults,
        topOperatorItemProperties: this.topOperatorItemProperties,
        topOperatorItemWidth: this.topOperatorItemWidth,

        showSelectionCheckbox: this.showSelectionCheckbox,
        negated: negated,
        filterBuilder: this,
        width: "100%",

        inheritedClauseProperties:this.inheritedClauseProperties
      })
    );

    var rtnVal = this._addClause(filterClause);
    filterClause.updateInlineTopOperator();
    return rtnVal;
},

//> @method filterBuilder.addClause()
// Add a new +link{FilterClause} to this FilterBuilder.
// <P>
// This API is intended for the rare use case of adding a highly customized FilterClause
// component that does not include the standard field/operator/value picking interface, instead
// providing a custom interface and returning a criterion via +link{filterClause.getCriterion()}.
// <P>
// If you just want to programmatically add a new FilterClause showing a specific Criterion use
// +link{addCriterion()}.
// <P>
// If you want to use the standard field/operator/value interface but provide a custom control
// for editing the value, see +link{DataSource.addSearchOperator} and +link{Operator.editorType}.
//
// @param filterClause (FilterClause) A +link{FilterClause} instance
// @visibility external
//<
addClause : function (filterClause) {
    // add the passed filterClause
    if (!filterClause) return filterClause;

    var _this = this;


    filterClause.fieldDataSource = this.fieldDataSource;
    filterClause.remove = function () {
        _this.removeClause(this);
    };
    filterClause.fieldNameChanged = function () {
        this.Super("fieldNameChanged", arguments);
        _this.fieldNameChanged(this);
    };

    var rtnVal = this._addClause(filterClause);
    filterClause.updateInlineTopOperator();
    return rtnVal;
},

_addClause : function (filterClause) {
    filterClause.filterBuilder = this;

    // Refresh the clause's fields so it can
    // pick up context from the filterBuilder
    filterClause.updateFields();
    this.clauses.add(filterClause);

    var clauseStack = this.clauseStack,
        position = Math.max(0, clauseStack.getMemberNumber(this.buttonBar)),
        _this = this
    ;

    clauseStack.addMember(filterClause, position);
    clauseStack.showMember(filterClause,
        function () {
            if (!_this._settingCriteria) filterClause.setDefaultFocus();
        }
    );

    this.updateFirstRemoveButton();
    // fire filterChanged
    if (isc.isA.Function(this.filterChanged)) this.filterChanged();
    return filterClause;
},

//> @method filterBuilder.getChildFilters()
// Returns an array of child +link{class:FilterBuilder}s, representing the list of complex
// clauses, or an empty array if there aren't any.
//
// @return (Array of FilterBuilder) The list of complex clauses for this filterBuilder
// @visibility external
//<
getChildFilters : function () {
    var childFilters = [];

    for (var i = 0; i<this.clauses.length; i++) {
        var filter = this.clauses[i];
        if (isc.isA.FilterBuilder(filter)) childFilters.add(filter);
    }

    return childFilters;
},


//> @method filterBuilder.getFilterDescription()
// Returns a human-readable string describing the clauses in this filterBuilder.
//
// @return (String) Human-readable string describing the clauses in the passed criteria
// @visibility external
//<
getFilterDescription : function () {
    return isc.DataSource.getAdvancedCriteriaDescription(this.getCriteria(),
            // getAdvancedCriteriaDescription handles being passed an array of DS's and criteria with
            // a dataSource.fieldName type fieldName.
            (this.dataSources || this.dataSource));
},

//> @attr filterBuilder.rangeSeparator (String : "and" : IR)
// For operators that check that a value is within a range, text to show between the start and
// end input fields for specifying the limits of the range.
// @visibility external
// @group i18nMessages
//<
rangeSeparator: "and",

//> @method filterBuilder.validate
// Validate the clauses of this FilterBuilder.
// @return (Boolean) true if all clauses are valid, false otherwise
// @visibility external
//<
validate : function () {
    var valid = true;
    for (var i = 0; i < this.clauses.length; i++) {
        if (!this.clauses[i].validate(null, null, true)) valid = false;
    }
    return valid;
},


//> @method filterBuilder.getFieldOperators()
// Get the list of +link{OperatorId, operatorIds} that are valid for the passed field.  By
// default, all operators returned by +link{dataSource.getFieldOperators()} are used.
// <P>
// Called automatically by the default implementation of the same method on each
// +link{filterClause.getFieldOperators, clause}, whenever its fieldName is changed.
//
// @param fieldName (String) the name of the field for which to return the set of available operators
// @return (Array of OperatorId) valid operators for this field
// @visibility external
//<
getFieldOperators : function (fieldName, field) {

    var ds = this.getPrimaryDS(fieldName);
    return ds ? ds.getFieldOperators(field || fieldName) : null;
},

//> @method filterBuilder.getValueFieldProperties()
//
// Override to return properties for the FormItem(s) used for the "value" field displayed within
// clauses within this filterBuilder.
// <P>
// Note that the +link{operator.valueType} impacts when this method is called. For operators with
// valueType <code>"fieldType"</code> or <code>"custom"</code>, a single value field is displayed.
// For operators with valueType <code>"valueRange"</code> two value-field items are displayed
// (one for the start and one for the end position). The <code>valueItemType</code> parameter may
// be used to determine which form item is being generated.
//
// @param type (FieldType) type of the DataSource field for this filter row
// @param fieldName (String) name of the DataSource field for this filter row
// @param operatorId (OperatorId) +link{OperatorId} for the chosen operator
// @param itemType (ValueItemType) What valueItem is being generated.
//
// @return (FormItem Properties) properties for the value field
// @visibility external
//<
getValueFieldProperties : function (type, fieldName, operatorId, itemType) {
    // This is recursive - this filterBuilder could be a subclause of a parent filterBuilder!
    if (this.filterBuilder) {
        return this.filterBuilder.getValueFieldProperties(type,fieldName,operatorId,itemType);
    }
},


getRangeSeparatorProperties : function (type, fieldName, operatorId) {
    return this.rangeSeparatorProperties;
},


// Subclauses
// ---------------------------------------------------------------------------------------

//> @attr filterBuilder.showSubClauseButton (Boolean : See Description : IR)
// Whether to show a button that allows the user to add subclauses.  Defaults to false if
// the +link{topOperatorAppearance} is "radio" or "inline", true in all other cases.
// @visibility external
//<

//> @attr filterBuilder.subClauseButtonTitle (string : "+()" : IR)
// The title of the subClauseButton
//
// @group i18nMessages
// @visibility external
//<
subClauseButtonTitle: "+()",

//> @attr filterBuilder.subClauseButtonPrompt (string : "Add Subclause" : IR)
// The hover prompt text for the subClauseButton.
//
// @group i18nMessages
// @visibility external
//<
subClauseButtonPrompt: "Add Subclause",

//> @attr filterBuilder.subClauseButton (AutoChild IButton : null : IR)
// Button allowing the user to add subclauses grouped by a +link{type:LogicalOperator}.
// @visibility external
//<
subClauseButtonDefaults : {
    _constructor:"IButton",
    autoParent:"buttonBar",
    //title:"+()", // need an icon for this
    autoFit:true,
    //prompt:"Add Subclause",
    click : function () { this.creator.addSubClause(this.clause); }
},

//> @attr filterBuilder.bracket (MultiAutoChild Canvas : null : IR)
// Widget used as a "bracket" to hint to the user that a subclause groups several
// field-by-field filter criteria under one logical operator.
// <P>
// By default, a simple CSS-style Canvas with borders on three sides.  A vertical StretchImg
// could provide a more elaborate appearance.
// @visibility external
//<
bracketDefaults : {
    styleName:"bracketBorders",
    width:10
},

childResized : function () {
    this.Super("childResized", arguments);
    if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},
draw : function () {
    this.Super("draw", arguments);
    if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},
resized : function () {
    if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},

addSubClause : function (criterion) {
    var operator;
    if (criterion) {
        operator = criterion.operator;
    }
    var clause = this.createAutoChild("subClause", {
        dataSource:this.dataSource,
        dataSources:this.dataSources,
        showFieldTitles:this.showFieldTitles,
        filterBuilder:this,
        parentClause:this,
        retainValuesAcrossFields: this.retainValuesAcrossFields,
        topOperatorAppearance:"bracket",
        topOperator: operator || this.defaultSubClauseOperator || this.topOperator,
        topOperatorOptions: this.topOperatorOptions,
        clauseConstructor: this.clauseConstructor,

        topOperatorFormDefaults:this.topOperatorFormDefaults,
        topOperatorFormProperties:this.topOperatorFormProperties,
        topOperatorItemDefaults:this.topOperatorItemDefaults,
        topOperatorItemProperties:this.topOperatorItemProperties,
        topOperatorItemWidth: this.topOperatorItemWidth,

        fieldPickerDefaults: this.fieldPickerDefaults,
        fieldPickerProperties: this.fieldPickerProperties,

        subClauseButtonTitle: this.subClauseButtonTitle,
        subClauseButtonPrompt: this.subClauseButtonPrompt,

        fieldPickerWidth: this.fieldPickerWidth,
        operatorPickerWidth: this.operatorPickerWidth,
        valueItemWidth: this.valueItemWidth,

        operatorPickerProperties: this.operatorPickerProperties,
        operatorPickerDefaults: this.operatorPickerDefaults,

        fieldDataSource: this.fieldDataSource,
        fieldData: this.fieldData,
        // copy sortFields onto the 'clause' so buildValueItemList can
        // apply the appropriate defaults to the pickList
        sortFields:this.sortFields,
        showRemoveButton: this.showRemoveButton,
        showAddButton: this.showAddButton,
        showSubClauseButton: this.showSubClauseButton,
        showTopRemoveButton: this.showSubClauseButton,
        visibility:"hidden",
        saveOnEnter: this.saveOnEnter,
        validateOnChange: this.validateOnChange,
        // We don't need (or want) to create empty children of new subclauses if we're
        // building up the UI from a passed-in AdvancedCriteria
        dontCreateEmptyChild: criterion != null,

        // Forward all filterChanged() events to the top-most FilterBuilder.
        filterChanged : function () {
            var filterBuilder = this.filterBuilder;
            while (filterBuilder.filterBuilder != null) {
                filterBuilder = filterBuilder.filterBuilder;
            }
            // Fire filterChanged
            if (isc.isA.Function(filterBuilder.filterChanged)) {
                filterBuilder.filterChanged();
            }
        },
        // Forward all getFieldOperators() calls to the top-most FilterBuilder.
        getFieldOperators : function (fieldName, field) {
            var filterBuilder = this.filterBuilder;
            while (filterBuilder.filterBuilder != null) {
                filterBuilder = filterBuilder.filterBuilder;
            }
            // Fire return the result of getFieldOperators on the top-most filterBuilder
            if (isc.isA.Function(filterBuilder.getFieldOperators)) {
                return filterBuilder.getFieldOperators(fieldName, field);
            }
        },
        // This object allows us to set top level properties which will be
        // applied to each nested FilterClause we create, recursively down subclauses.
        inheritedClauseProperties:this.inheritedClauseProperties
    }, this.Class);

    this.clauses.add(clause);

    this.clauseStack.addMember(clause, this.clauses.length-1);
    this.clauseStack.showMember(clause, function () {
        clause.topOperatorForm.focusInItem("operator");
        clause.bracket.setHeight(clause.getVisibleHeight());
    });

    // update the firstRemoveButton on the containing clause
    this.updateFirstRemoveButton();

    return clause;
},


// Deriving AdvancedCriteria
// ---------------------------------------------------------------------------------------

//> @method filterBuilder.getCriteria()
// Get the criteria entered by the user.
//
// @param [includeEmptyValues] (boolean) By default if a user has selected a field and operator
//   type, but has failed to enter a value for the field it will be skipped. This optional parameter
//   allows you to retrieve all criteria, including those with an empty <code>value</code> attribute.
// @return (AdvancedCriteria)
// @visibility external
//<
getCriteria : function (includeEmptyValues) {
    if (this._initializingClauses) {
        // if we were initialized with criteria and the clauses are still being created, just
        // return the criteria we were initialized with
        return this.criteria;
    }

    if (this.topOperatorAppearance == "inline") {
        return this.getInlineCriteria(includeEmptyValues);
    }

    var criteria = {
        _constructor:"AdvancedCriteria",
        operator:this.topOperator,
        criteria:[]
    };

    for (var i = 0; i < this.clauses.length; i++) {
        var clause = this.clauses[i],
            criterion,
            skipCriterion = false;

        if (isc.isA.FilterBuilder(clause)) {
            criterion = clause.getCriteria(includeEmptyValues);
        } else {
            criterion = clause.getCriterion(includeEmptyValues);
            skipCriterion = (criterion == null);
        }
        if (!skipCriterion) {
            criteria.criteria.add(criterion);
        }
    }
    // Return a copy - the original contains pointers to the live screen objects
    return isc.clone(criteria);
},

getInlineCriteria : function (includeEmptyValues) {
    var criteria = {
        _constructor:"AdvancedCriteria",
        operator:this.topOperator,
        criteria:[]
    };

    if (this.topOperator == "or") {
        // If we have any "and not"s, we need to bundle everything under a top-level
        // "and"
        var orAndNot;
        for (var i = 0; i < this.clauses.length; i++) {
            if (this.clauses[i].topOperatorForm.getValue("operator") == "not") {
                orAndNot = true;
                break;
            }
        }
        if (orAndNot) {
            criteria.operator = "and";
            var orCriteria = {
                operator: "or",
                criteria: []
            }
            criteria.criteria.add(orCriteria);
        }
    }

    for (var i = 0; i < this.clauses.length; i++) {
        var clause = this.clauses[i];
        var operator = clause.topOperatorForm.getValue("operator");
        if (!operator || operator == this.topOperator) {
            if (orAndNot) {
                orCriteria.criteria.add(clause.getCriterion(includeEmptyValues));
            } else {
                criteria.criteria.add(clause.getCriterion(includeEmptyValues));
            }
        } else {
            criteria.criteria.add({
                operator: "not",
                criteria: [ clause.getCriterion(includeEmptyValues) ]
            });
        }
    }
    return criteria;
},

// fired when this builder is ready for interactive use
filterReady : function () { },

//> @method filterBuilder.setCriteria()
// Set new criteria for editing.
// <P>
// An interface for editing the provided criteria will be generated identically to what happens
// when initialized with +link{criteria}.
// <P>
// Any existing criteria entered by the user will be discarded.
//
// @param criteria (AdvancedCriteria) new criteria.  Pass null or {} to effectively reset the
//                                    filterBuilder to it's initial state when no criteria are
//                                    specified
// @visibility external
//<
setCriteria : function (criteria) {

    this.clearCriteria(true);

    var animation = this.clauseStack ? this.clauseStack.animateMembers : null;
    if (this.clauseStack) this.clauseStack.animateMembers = false;

    this.stripNullCriteria(criteria);

    this._settingCriteria = true;

    if (!this._loadingFieldData && this.fieldDataSource && criteria) {
        // fetch the necessary field-entries so they can be passed into the filterClauses
        if (isc.isA.String(this.fieldDataSource)) {
            this.fieldDataSource = isc.DS.getDataSource(this.fieldDataSource);
        }

        var _this = this,
            fieldsInUse = this.fieldDataSource.getCriteriaFields(criteria),
            fieldCriteria = {}
        ;

        if (fieldsInUse && fieldsInUse.length > 0) {
            // construct an advanvcedCriteria to use when requesting used fields from the
            // fields DS
            fieldCriteria = { _constructor: "AdvancedCriteria", operator: "or", criteria: [] };
            for (i=0; i<fieldsInUse.length; i++) {
                var fieldName = fieldsInUse[i],
                    cachedField = this.fieldData ? this.fieldData[fieldName] : null;

                if (!cachedField) {
                    fieldCriteria.criteria[fieldCriteria.criteria.length] =
                        { fieldName: "name", operator: "equals", value: fieldName };
                }
            }

            if (fieldCriteria.criteria.length != 0) {
                this._loadingFieldData = true;
                this.fieldDataSource.fetchData(fieldCriteria,
                    function (data) {
                        _this.fetchFieldsReply(data, criteria);
                    }
                );
                return;
            }
        }
    }

    if (!criteria) {
        if (!this.allowEmpty && !this.dontCreateEmptyChild) this.addNewClause();
        this.clauseStack.show();
        this.redraw();
        this.filterReady();
        return;
    }


    if (!this.getPrimaryDS().isAdvancedCriteria(criteria)) {
        // The textMatchStyle we pass here is kind of arbitrary...
        criteria = isc.DataSource.convertCriteria(criteria, "substring");
    }

    if (this.topOperatorAppearance == "inline") {
        return this.setInlineCriteria(criteria, animation);
    }

    this.setTopOperator(criteria.operator);

    if ((!criteria.criteria || criteria.criteria.length == 0) &&
        !this.topOperatorOptions.contains(criteria.operator))
    {
        // AdvancedCriteria can validly consist of just an operator like lessThan and a field,
        // but the FilterBuilder assumes a top-level logical operator and need conversion for
        // this case
        this.logWarn("Found top-level AdvancedCriteria with no sub-criteria. Converting " +
                     "to a top-level 'and' with a single sub-criterion");
        this.setTopOperator(this.topOperator);
        this.addNewClause(criteria);
    } else {
        for (var i = 0; i < criteria.criteria.length; i++) {
            var criterion = criteria.criteria[i],
                field = this.fieldData ? this.fieldData[criterion.fieldName] : null;
            this.addCriterion(criterion, field);
        }
        // possible in the trivial case of a top-most operator of "add" and an empty set of
        // criteria
        if (this.clauses.length == 0 && !this.allowEmpty) this.addNewClause();
    }

    delete this._initializingClauses;
    this._loadingFieldData = false;
    this._settingCriteria = false;
    this.clauseStack.show();
    this.delayCall("redraw");

    if (this.clauseStack) this.clauseStack.animateMembers = animation;
    this.filterReady();
},

setInlineCriteria : function (criteria, animation) {
    // Inline AdvancedCriteria are expected to have a certain structure, so we'll verify that
    // first and crash out if it is not as expected
    var ok = true,
        andMode = false,
        orMode = false;
    if (criteria.operator == "and") {
        for (var i = 0; i < criteria.criteria.length; i++) {
            var clause = criteria.criteria[i];
            if (!clause.criteria) {
                andMode = true;
            } else {
                if (clause.operator == "or") {
                    orMode = true;
                    for (var j = 0; j < clause.criteria.length; j++) {
                        var subClause = clause.criteria[j];
                        if (subClause.criteria) {
                            ok = false;
                            break;
                        }
                    }
                } else {
                    if (clause.operator == "not") {
                        if (clause.criteria.length != 1 || clause.criteria[0].criteria) {
                            ok = false;
                        }
                    } else {
                        ok = false;
                    }
                }
            }
        }
    } else {
        ok = false;
    }

    // We must be in one of andMode or orMode, but not both
    if (ok) ok = !(andMode && orMode);
    if (ok) ok = andMode || orMode;

    if (!ok) {
        isc.logWarn("Trying to load an AdvancedCriteria into an 'inline' FilterBuilder, but " +
                    "the criteria is too complex to be represented in 'inline' format");
        return;
    }

    this.setTopOperator(andMode ? "and" : "or");
    if (andMode) {
        for (var i = 0; i < criteria.criteria.length; i++) {
            var clause = criteria.criteria[i],
                field = this.fieldData ? this.fieldData[clause.fieldName] : null;
            if (!clause.criteria) {
                this.addCriterion(clause, field);
            } else {
                // It must be a not with a single subclause
                field = this.fieldData ? this.fieldData[clause.criteria[0].fieldName] : null;
                this.addNewClause(clause.criteria[0], field, true);
            }
        }
    } else {
        // orMode
        for (var i = 0; i < criteria.criteria.length; i++) {
            var clause = criteria.criteria[i],
                field = this.fieldData ? this.fieldData[clause.fieldName] : null;
            if (clause.operator == "or") {
                for (var j = 0; j < clause.criteria.length; j++) {
                    // We've already tested that no subClause of the "or" group is itself a
                    // criteria group
                    var subClause = clause.criteria[j];
                    field = this.fieldData ? this.fieldData[subClause.fieldName] : null;
                    this.addCriterion(subClause, field);
                }
            } else {
                // It must be a not with a single subclause
                field = this.fieldData ? this.fieldData[clause.criteria[0].fieldName] : null;
                this.addNewClause(clause.criteria[0], field, true);
            }
        }
    }

    delete this._initializingClauses;
    this._loadingFieldData = false;
    this._settingCriteria = false;
    this.clauseStack.show();
    this.delayCall("redraw");

    if (this.clauseStack) this.clauseStack.animateMembers = animation;

    this.filterReady();

},

stripNullCriteria : function (criteria) {
    if (criteria && criteria.criteria && criteria.criteria.length>0) {
        for (var i = criteria.criteria.length-1; i>=0; i--) {
            if (criteria.criteria[i] == null) {
                criteria.criteria.removeAt(i);
            } else {
                if (criteria.criteria[i].criteria) this.stripNullCriteria(criteria.criteria[i]);
            }
        }
    }
},

fetchFieldsReply : function (data, criteria) {
    if (this.fieldData) {
        var newFields = isc.getValues(this.fieldData);
        newFields.addList(data.data);
        this.fieldData = newFields.makeIndex("name");
    } else this.fieldData = data.data.makeIndex("name");

    this.setCriteria(criteria);
},

//> @method filterBuilder.clearCriteria()
// Clear all current criteria.
// @visibility external
//<
clearCriteria : function (dontCheckEmpty) {

    var animation = this.clauseStack ? this.clauseStack.animateMembers : null;
    if (this.clauseStack) this.clauseStack.animateMembers = false;

    while (this.clauses.length > 0) {
        this.removeClause(this.clauses[0]);
    }

    if (!dontCheckEmpty && !this.allowEmpty) this.addNewClause();

    if (this.clauseStack) this.clauseStack.animateMembers = animation;
},

//> @method filterBuilder.addCriterion()
// Add a new criterion, including recursively adding sub-criteria for a criterion that
// contains other criteria.
//
// @param criterion (Criterion) new criterion to be added
// @visibility external
//<
addCriterion : function (criterion, field) {

    if (criterion.criteria) {
        var clause = this.addSubClause(criterion);
        for (var idx = 0; idx < criterion.criteria.length; idx++) {
            field = this.fieldData ? this.fieldData[criterion.criteria[idx].fieldName] : null;
            clause.addCriterion(criterion.criteria[idx], field);
        }
    } else {
        this.addNewClause(criterion, field);
    }

},

_$Enter:"Enter",
handleKeyPress: function (event, eventInfo){

    // Special case for Enter keypress: If this.saveOnEnter is true, and the enter keypress
    // occurred in a text item, and this is a top-level FilterBuilder with a search() method
    // defined, call the search() method and stop bubbling
    if (event.keyName == this._$Enter) {
        if (this.saveOnEnter) {
            if (eventInfo.firedOnTextItem) {
                if (!this.creator && this.search) {
                    this.search(this.getCriteria());
                    return isc.EH.STOP_BUBBLING;
                }
            }
        }
    }
},

itemChanged : function () {
    if (this.creator && isc.isA.Function(this.creator.itemChanged)) {
        this.creator.itemChanged();
    } else {
        if (!this.creator && isc.isA.Function(this.filterChanged)) {
            this.filterChanged();
        }
    }
},

fieldNameChanged : function (filterClause) {
},

// switch this on to allow the emptyValue entry in valueMapped fields
allowEmptyValues: false,

//> @method FilterBuilder.getEditorType()
// Returns the type of editor to use for the field.
// <P>
// Default behavior is to use the +link{operator.editorType} for a custom operator, otherwise,
// use +link{RelativeDateItem} for before/after/between operators on date fields, otherwise,
// use the same editor as would be chosen by a +link{SearchForm}.
//
// @param field (DataSourceField) DataSourceField definition
// @param operatorId (OperatorId) +link{OperatorId} for the chosen operator
// @return (SCClassName) SmartClient class to use (must be subclass of FormItem)
// @visibility external
//<
getEditorType : function (field, operatorId) {
    var ds = this.getPrimaryDS(field);
    return isc.FilterBuilder.getDefaultEditorType(field, ds, operatorId);
},


//> @method FilterBuilder.getSelectedClauses()
// Returns the list of this FilterBuilder's FilterClauses that are currently selected.  A
// clause is "selected" if the user has checked the checkbox next to it; therefore, this
// method always returns an empty list unless the
// +link{showSelectionCheckbox,showSelectionCheckbox} property is set.  This method is only
// applicable where +link{topOperatorAppearance} is "inline" (because that is the only
// appearance that supports <code>showSelectionCheckbox</code>)
//
// @return (Array of FilterClause) The list of selected clauses
// @visibility external
//<
getSelectedClauses : function () {
    var list = [];
    if (this.showSelectionCheckbox) {
        for (var i = 0; i < this.clauses.length; i++) {
            var c = this.clauses[i];
            if (c.topOperatorForm && c.topOperatorForm.getValue("select")) {
                list.add(c);
            }
        }
    }
    return list;
}

});

isc.FilterBuilder.addClassMethods({

// Static method to determine the editorType to show for the "value" item in a filterClause
// given a field/operator pair.
// For range type operators this is used for both the min and max item

getDefaultEditorType : function  (field, ds, operatorId) {
    var editorType;


    var dupField = {};
    isc.addProperties(dupField, field);
    dupField.canEdit = field.canFilter;
    field = dupField;

    var operator = ds.getSearchOperator(operatorId, field);
    // return the operator's editorType, if it has one
    if (operator.editorType) {
        editorType = operator.editorType;

    } else if (operator.getEditorType && isc.isA.Function(operator.getEditorType)) {
        editorType = operator.getEditorType();

    } else {
        var valueType = operator ? operator.valueType : "text";
        // another field in the same DataSource
        if (valueType == "fieldName") {
            // Return explicit null - there is logic at the Clause level to handle
            // using a ComboBoxItem if there is a fieldDataSource, otherwise a SelectItem.
            editorType = null;


        } else if (valueType == "valueSet") {
            editorType = "TextAreaItem";

        } else if (valueType == "custom" && operator && operator.editorType) {
            editorType = operator.editorType;
        }


        if (field) {
            // create a SearchForm instance so we can use completely standard SearchForm
            // editor creation logic


            // special case most operators on dates because the default behavior of using whatever a
            // SearchForm would show would actually show a date range control
            // Handled via the searchForm "defaultDateEditorType" attribute.
            var defaultDateItem =
                (operatorId == "equals" || operatorId == "notEqual" ||
                operatorId == "lessThan" || operatorId == "greaterThan" ||
                // insensitive operators don't really apply to dates but this check is here in case
                // they are explicitly supplied by a developer
                operatorId == "iBetween" || operatorId == "iBetweenInclusive" ||
                operatorId == "between" || operatorId == "betweenInclusive" ||
                operatorId == "greaterOrEqual" || operatorId == "lessOrEqual")
                        ? "RelativeDateItem" : "DateRangeItem";

            if (!this.internalSearchForm) {

                this.internalSearchForm = isc.SearchForm.create({
                    visibility: "hidden",
                    autoDraw: false,
                    useAllDataSourceFields:false, dataSource: ds, fields:[field],
                    defaultDateEditorType:defaultDateItem
                    //allowEmptyValues: this.allowEmptyValues
                });

            } else {
                this.internalSearchForm.defaultDateEditorType = defaultDateItem;
                // Note: pass the field direclty to setDataSource() and not via a discrete
                // setFields() to improve performance
                this.internalSearchForm.setDataSource(ds, [field]);
            }
            editorType = this.internalSearchForm.getEditorType(field);
        } else {
            editorType = isc.FormItemFactory.getItemClassName({}, "text", null);
        }
    }

    return editorType;
}

});

isc.FilterBuilder.registerStringMethods({

    //> @method filterBuilder.search()
    // A StringMethod that is automatically invoked if +link{filterBuilder.saveOnEnter} is set
    // and the user presses Enter whilst in a text-item in any clause or subclause.
    //
    // @param criteria (AdvancedCriteria) The criteria represented by the filterBuilder
    // @visibility external
    //<

    search : "criteria",

    //> @method filterBuilder.filterChanged()
    // Handler fired when there is a change() event fired on any FormItem within the
    // filterBuilder.
    //
    // @visibility external
    //<

    filterChanged : ""
});


} // End of if (isc.DynamicForm)







//>    @class    MockupElement
// MockupElements are produced by the +link{group:balsamiqImport,Balsamiq Mockup Importer} as
// placeholders for Balsamiq controls that cannot be meaningfully translated to SmartClient
// controls (such as the big red X markup control).
// <p>
// MockupElement is just an instance of Img that uses .png files stored in the
// tools/visualBuilder/mockups folder.
// <p>
// MockupElement is not intended to be included in any final applications.
//
// @treeLocation Client Reference/Tools
// @visibility external
//<


isc.overwriteClass("MockupElement", "Img");

isc.MockupElement.addProperties({
    controlName:"MockupElement",
    defaultWidth:16,
    defaultHeight:28,
    measuredW:-1,
    measuredH:-1
});

isc.MockupElement.addMethods({
    initWidget : function () {
        this.Super(this._$initWidget);
        var url = "/tools/visualBuilder/mockups/";
        var postfix = this.controlName.substr(this.controlName.indexOf("::") + 2,
            this.controlName.length) + ".png";
        this.src=url + postfix;
        if (this.title != null) {
            this.addChild(
                isc.Label.create({
                    ID:this.getID() + "_titleLabel",
                    autoDraw:true,
                    left: 10,
                    top: 0,
                    width: this.width,
                    height: this.height,
                    zIndex: this.getZIndex(true) + 1,
                    contents: this.title
                })
            );
        }
    }
});


//> @class RuleEditor
// A user-interface component for creation and editing of a +link{Rule} or +link{Validator}.
// @treeLocation Client Reference/Rules
// @visibility rules
//<
isc.defineClass("RuleEditor", "VLayout");


isc.RuleEditor.addProperties({

    // ----
    // Basics / Attributes
    // ----

    // Default height to explicit size. This will give it "implicit height" and stop it
    // expanding in Layouts to fill available space.
    height:100,

    //> @attr ruleEditor.rule (Validator : null : IRW)
    // Rule to be edited by this ruleEditor. Use +link{setRule} and +link{getRule} to
    // update or retrieve this object at runtime.
    // @visibility rules
    //<

    //> @attr ruleEditor.validator (Validator : null : IRW)
    // Validator to be edited by this ruleEditor. Synonym for +link{ruleEditor.rule}.
    // Use +link{setRule} and +link{getRule} to update or retrieve this object at runtime.
    // @visibility rules
    //<

    //> @attr ruleEditor.fieldName  (String : null : IRW)
    // Name of the field to which the rule applies. If +link{fieldPicker} is visible, this may
    // be chosen by the user. The set of fieldNames displayed to the user will be derived from
    // +link{ruleEditor.dataSource} or +link{ruleEditor.dataSources}.
    // @visibility rules
    //<


    //> @attr ruleEditor.dataSource (DataSource : null : IR)
    // DataSource where this validator will be applied. The +link{fieldName} should refer
    // to a field within this dataSource. Should not be set in conjunction with +link{dataSources}.
    // @visibility rules
    //<

    //> @attr ruleEditor.dataSources (Array of DataSource : null : IR)
    // DataSources available to this rule when defining a rule. The +link{fieldName} should
    // refer to a field within these dataSource, using notation of the form
    // <code><i>dataSourceID</i>.<i>fieldName</i></code>. Should not be set in conjunction with
    // +link{dataSource}.
    // @visibility rules
    //<

    //> @attr ruleEditor.locator (AutoTestObjectLocator : null : IRW)
    // Optional locator to be stored as +link{rule.locator} for the rule being edited.
    // This locator may be absolute, or may be relative to a base component. If a relative
    // object locator is used, when the generated rule is applied to a +link{RulesEngine},
    // the RulesEngine would be expected to have +link{rulesEngine.baseComponent} set such
    // that the relative locator can be resolved to a live object at runtime.
    // If +link{fieldPicker} is visible, this may be chosen by the user from the options
    // provided as +link{ruleEditor.locatorMap}.
    // <P>
    // See +link{AutoTest.getObjectLocator()} and +link{AutoTest.getRelativeObjectLocator()} for
    // more on object locators.
    //
    // @visibility rules
    //<

    //> @attr ruleEditor.locatorMap (ValueMap : null : IRW)
    // Set of possible +link{type:AutoTestObjectLocator}s to be shown in the +link{fieldPicker}
    // for this rule editor. This allows the user to specify the +link{rule.locator} for
    // the rule being edited.
    // <P>
    // This property is a ValueMap - it may be specified as an array of AutoTestObjectLocator
    // string, or as an object mapping of locator strings to display values.
    // <P>
    // See +link{AutoTest.getObjectLocator} and +link{AutoTest.getRelativeObjectLocator()} for
    // information on getting object locators from live SmartClient objects.
    //
    // @visibility rules
    //<

    //> @attr ruleEditor.triggerEvent (TriggerEvent : null : IRW)
    // If this ruleEditor is editing a rule to be applied via a +link{rulesEngine},
    // what +link{rule.triggerEvent} is assigned to the rule?
    // +link{triggerEventPicker}
    // @visibility rules
    //<
    // This may be specified programmatically or picked by the user via the triggerEventPicker

    //> @attr ruleEditor.validatorType (ValidatorType : null : IRW)
    // Type of validator being edited. If +link{showTypePicker} is true, this may be chosen
    // by the user.
    // @setter setValidatorType
    // @visibility rules
    //<

    //> @attr ruleEditor.validatorIsRule (boolean : true : IRA)
    // Is this ruleEditor editing a rule to be applied via a +link{rulesEngine}, or
    // modifying a validator to be applied directly to a field.
    // @visibility rules
    //<

    validatorIsRule:true,

    //> @attr ruleEditor.availableTypes (Array of ValidatorType : [...] : IR)
    // List of available validator types.  Defaults to all validator types and rule types
    // that do not require input of a custom expression (eg "requiredIf"), excluding validators
    // that just verify the field type and are usually implicit (isBoolean, isString, etc).
    // <P>
    // The special value "range" may be specified to indicate that the appropriate "range"
    // validator for the +link{ruleEditor.field,field type} (integerRange, dateRange, etc) should
    // be used.
    // @visibility rules
    //<

    availableTypes:[
        "matchesField",
        "isOneOf",
        "lengthRange",
        "contains",
        "doesntContain",
        "substringCount",
        "regexp",
        "mask",
        "floatPrecision",
        "readOnly",
        "isUnique",
        "hasRelatedRecord",
        "range",
        "message",
        "populate",
        "setRequired"
    ],

    //> @attr ruleEditor.applyWhen (AdvancedCriteria : null : IRW)
    // Criteria indicating under what circumstances the rule should be applied.
    // @visibility rules
    //<

    //> @attr ruleEditor.nameItemTitle (string : "Name": IR)
    // Title of the name field.
    // @group i18nMessages
    // @visibility rules
    //<
    nameItemTitle:"Name",

    //> @attr ruleEditor.descriptionItemTitle (string : "Description": IR)
    // Title of the description field.
    // @group i18nMessages
    // @visibility rules
    //<
    descriptionItemTitle:"Description",

    //> @attr ruleEditor.triggerEventTitle (string : "On event": IR)
    // Title of the trigger event field.
    // @group i18nMessages
    // @visibility rules
    //<
    triggerEventTitle:"On event",

    //> @attr ruleEditor.fieldPickerTitle (string : "For fields": IR)
    // Title of the field picker field.
    // @group i18nMessages
    // @visibility rules
    //<
    fieldPickerTitle:"For field",

    //> @attr ruleEditor.applyWhenTitle (string : "If": IR)
    // Title of the applyWhen field.
    // @group i18nMessages
    // @visibility rules
    //<
    applyWhenTitle:"If",

    //> @attr ruleEditor.applyWhenPlaceholder (string : ".. conditional ..": IR)
    // Placeholder text displayed to right of the applyWhenTitle when unchecked.
    // @group i18nMessages
    // @visibility rules
    //<
    applyWhenPlaceholder:".. conditional ..",

    //> @attr ruleEditor.validatorTitle (string : "Do": IR)
    // Title of the validator (rule) field.
    // @group i18nMessages
    // @visibility rules
    //<
    validatorTitle:"Do",

    //> @attr ruleEditor.errorMessageTitle (string : "Message": IR)
    // Title of the errorMessage field.
    // @group i18nMessages
    // @visibility rules
    //<
    errorMessageTitle:"Message",


    // -------


    // default width to 400 - that's enough to accommodate the mainForm
    width:400,


    initWidget : function () {
        var initialRule = this.rule || this.validator;
        if (initialRule != null) {
            // this will derive fieldName etc from the rule object.
            this.setRule(initialRule, true);
        }

        // Default to hiding the typePicker if a validatorType is defined at init-time
        if (this.showTypePicker == null && this.validatorType != null) {
            this.showTypePicker = false;
        }

        // call addAutoChildren to build the UI. This will pick up dynamicDefaults from
        // the special 'getDynamicDefaults' method, and will handle custom UI being injected
        // into the layout.
        this.addAutoChildren(this.components);

        // set initial field values based on initial rule passed in.
        if (this.nameForm != null && initialRule != null) {
            this.nameForm.setValue("name", initialRule.name);
            this.nameForm.setValue("description", initialRule.description);
        }
        if (this.mainForm) {
            this.fieldPicker = this.mainForm.getItem("fieldName");
            if (this.fieldName != null) this.fieldPicker.setValue(this.fieldName);

            this.triggerEventPicker = this.mainForm.getItem("event");
            if (this.triggerEvent) this.triggerEventPicker.setValue(this.triggerEvent);

            // If validatorIsRule we show neither the fieldName nor the eventPicker, so
            // hide that entire form so it doesn't take up space.
            if (!this.validatorIsRule) {
                this.mainForm.setVisibility(isc.Canvas.HIDDEN);
            }
        }

        if (this.applyWhenForm) {
            // conditionalForm - configures the "applyWhen" block of the validator
            if (this.applyWhen != null) {
                this.applyWhenForm.setValue("applyWhen", true);
                this.updateConditionalForm(true);
            }
        }
        if (this.validatorForm) {
            this.typePicker = this.validatorForm.getItem("type");
            if (this.validatorType != null) this.typePicker.setValue(this.validatorType);


            if (this.validatorType != null) {
                this.updateValidatorType(this.validatorType, true);
            }
        }

        // Initialize 'errorMessage' value
        if (this.messageForm) {
            if (this.rule && this.rule.errorMessage) {
                this.messageForm.setValue("errorMessage", this.rule.errorMessage);
            }
        }

        // update the clause to show the initial 'value' field attributes etc if there
        // are any.
        if (initialRule != null) {
            this.setClauseAttributes(initialRule);
        }

        return this.Super("initWidget", arguments);
    },


    // ----
    // UI
    // ----


    //> @attr ruleEditor.components (Array of Object : [...] : IRA)
    // Member components of this rule editor. Default value is an array of auto-children
    // names (strings), but for custom UI, additional components may be explicitly added.
    // @visibility rules
    //<
    components:[
        "nameForm", "mainForm", "applyWhenForm", "validatorForm", "messageForm"
    ],

    getDynamicDefaults : function (childName) {
        switch (childName) {
            case "nameForm" :
                // nameForm autoChild - configures name and description
                var nameTitleProperties = (this.nameItemTitle ? {title:this.nameItemTitle} : null),
                    descTitleProperties = (this.descriptionItemTitle ? {title:this.descriptionItemTitle} : null),
                    nameItem = isc.addProperties({name:"name"},
                                    this.nameItemDefaults, this.nameItemProperties, nameTitleProperties),
                    descriptionItem = isc.addProperties({name:"description"},
                                    this.descriptionItemDefaults, this.descriptionItemProperties, descTitleProperties);

                return {items:[nameItem,descriptionItem]};

            case "mainForm" :

                // The picker fields follow the autoChildren pattern but we can't
                // use createAutoChild since the form will of course create the live item instances
                var fieldPickerTitleProperties = (this.fieldPickerTitle ? {title:this.fieldPickerTitle} : null),
                    triggerEventTitleProperties = (this.triggerEventTitle ? {title:this.triggerEventTitle} : null),
                    triggerEventItemValueMap = { valueMap:{
                        "editStart": this.editStartEventTitle,
                        "editStartAndChanged": this.editStartAndChangedEventTitle,
                        "editorEnter": this.editorEnterEventTitle,
                        "editorExit": this.editorExitEventTitle,
                        "changed": this.changedEventTitle,
                        "submit": this.submitEventTitle,
                        "manual": this.manualEventTitle
                    }},
                    fieldItem = isc.addProperties(
                        {creator:this, editorType:this.fieldPickerConstructor},
                         this.fieldPickerDefaults,
                         this.fieldPickerProperties,
                         fieldPickerTitleProperties
                    ),
                    triggerEventItem = isc.addProperties(
                        {creator:this, editorType:this.triggerEventPickerConstructor},
                        this.triggerEventPickerDefaults,
                        triggerEventItemValueMap,
                        this.triggerEventPickerProperties,
                        triggerEventTitleProperties
                    )

                return {
                    items:[fieldItem, triggerEventItem]
                };

            case "applyWhenForm" :
                var titleProperties = (this.applyWhenTitle ? {title:this.applyWhenTitle} : null),
                    applyWhenItem = isc.addProperties({name:"applyWhen"},
                            this.applyWhenItemDefaults, this.applyWhenItemDefaults, titleProperties),
                    placeholderItem = {type:"StaticTextItem", name:"placeholder", showTitle:false, value:this.applyWhenPlaceholder},
                    conditionalItem = {type:"CanvasItem", showTitle:false, name:"conditionalItem", showIf:"false",
                            createCanvas:function () {
                                return this.form.creator.createConditionalForm()
                            }
                    };

                return {
                    items:[applyWhenItem,placeholderItem,conditionalItem]
                };

            // - validatorForm
            //  o typeItem - for selecting the validator type
            //  o valuesForm (embedded in a CanvasItem) for configuring the validator.
            //    this is a filterClause
            case "validatorForm" :
                var titleProperties = (this.validatorTitle ? {title:this.validatorTitle} : null);
                var typeItem = isc.addProperties(
                        {creator:this, editorType:this.typePickerConstructor},
                         this.typePickerDefaults,
                         this.typePickerProperties,
                         titleProperties
                    );

                var valuesItem = {
                    name:"valuesItem",
                    editorType:"CanvasItem",
                    showTitle:true, title:null,
                    showIf:"false",
                    canvas:this.getValuesForm(this.validatorType)
                }

                return {
                    disabled:(this.fieldName == null),
                    items:[typeItem, valuesItem]
                };

            case "messageForm" :
                var titleProperties = (this.errorMessageTitle ? {title:this.errorMessageTitle} : null),
                    messageItem = isc.addProperties({name:"errorMessage"},
                                this.errorMessageItemDefaults, this.errorMessageItemDefaults, titleProperties);

                return {items:[messageItem]};
        }
    },


    //> @attr ruleEditor.showNameForm (boolean : false : IR)
    // Should we show the +link{ruleEditor.nameForm} for editing the <code>name</code> and
    // <code>description</code> attributes of the rule being edited?
    // @visibility rules
    //<
    showNameForm:false,

    //> @attr ruleEditor.nameForm (DynamicForm AutoChild : null : IR)
    // DynamicForm used to edit the <code>name</code> and <code>description</code> of the rule
    // being edited.
    // <P>
    // Contains the +link{ruleEditor.nameItem} and +link{ruleEditor.descriptionItem}
    //
    // @visibility rules
    //<
    // Name form - contains name and description
    nameFormConstructor:"DynamicForm",
    nameFormDefaults:{
        numCols:2
    },

    //> @attr ruleEditor.nameItem (TextItem AutoChild : {...} :IR)
    // Item for editing the +link{validator.name,name} of the rule being edited. Displayed
    // in the +link{ruleEditor.nameForm} (if +link{ruleEditor.showNameForm} is true).
    // Implemented as an autoChild, so may be customized via <code>nameItemProperties</code>
    // @visibility rules
    //<
    nameItemDefaults:{
        editorType:"TextItem"
    },

    //> @attr ruleEditor.descriptionItem (TextItem AutoChild : {...} :IR)
    // Item for editing the +link{validator.description,description} of the rule being edited.
    // Displayed in the +link{ruleEditor.nameForm} (if +link{ruleEditor.showNameForm} is true).
    // Implemented as an autoChild, so may be customized via <code>nameItemProperties</code>
    // @visibility rules
    //<
    descriptionItemDefaults:{
        editorType:"TextAreaItem"
    },


    // Main Form (FieldName / TriggerEvent)

    mainFormConstructor:"DynamicForm",
    mainFormDefaults:{
        numCols:2,
        height:20
    },

    //> @attr ruleEditor.fieldPicker (AutoChild FormItem : null : IR)
    // Field for picking +link{rule.fieldName}. This form item will only be visible if
    // the user is editing a rule (see +link{validatorIsRule}).
    // @visibility rules
    //<

    fieldPickerConstructor:"SelectItem",

    fieldPickerDefaults:{
        name:"fieldName",
        multiple:true,
        showIf:function () {
            return this.creator.shouldShowFieldPicker();
        },
        pickListProperties:{
            showHeader:true,
            canSelectAll:false
        },
        getClientPickListData:function () {
            var pickListData = [];
            var locatorMap = this.form.creator.locatorMap;
            if (locatorMap != null) {
                if (isc.isAn.Array(locatorMap)) {
                    for (var i = 0; i < locatorMap.length; i++) {
                        pickListData.add({
                            name:locatorMap[i],
                            title:locatorMap[i],
                            type:"locator"
                        });
                    }
                } else {
                    for (var locator in locatorMap) {
                        pickListData.add({
                            name:locator,
                            title:locatorMap[locator],
                            type:"locator"
                        });
                    }
                }
            }
            var fieldData = this.form.creator.getFieldData();
            pickListData.addList(fieldData);
            return pickListData;
        },
        valueField:"name",
        displayField:"title",


        pickListWidth:300,
        // show the type so its obvious what's going on when we filter by type.
        pickListFields:[
            {name:"title", title:"Target", width:80},
            {name:"type", title:"Type", width:80},
            {name:"name", title:"Identifier", autoFitWidth:true}
        ],
        changed : function (form,item,value) {
            // force a refilter to show only fields that match the specified type.
            if (this.pickList && this.pickList.isVisible()) this.filterPickList();
            var locatorValues = null,
                fieldValues = null;
            if (value != null) {
                if (!isc.isAn.Array(value)) value = [value];
                for (var i = 0; i < value.length; i++) {

                    var field = this.form.creator.getField(value[i], true);
                    if (field == null) {
                        if (locatorValues == null) locatorValues = [];
                        locatorValues.add(value[i]);
                    } else {
                        if (fieldValues == null) fieldValues = [];
                        fieldValues.add(value[i]);
                    }
                }
            }

            if (this.creator.warnOnInvalidFieldSelection &&
                value != null && !isc.isAn.emptyArray(value) &&
                (this.creator.getSupportedTypes(fieldValues, locatorValues).length == 0))
            {
                isc.warn(this.creator.invalidFieldSelectionWarning);
                // if we did this on the change rather than changed handler we could return false
                // and suppress the change.
            }
            this.creator.updateFieldName(fieldValues, locatorValues);
        }
    },

    //> @attr ruleEditor.warnOnInvalidFieldSelection (boolean : true : IRW)
    // If the +link{fieldPicker} is showing and the user selects a combination of
    // field(s) and locator(s) that will not be supported by any
    // +link{availableTypes,specified rule type}, should we show a warning to the user?
    // +link{invalidFieldSelectionWarning} can be used to customize the warning text.
    // @visibility rules
    //<
    warnOnInvalidFieldSelection:true,

    //> @attr ruleEditor.invalidFieldSelectionWarning (string : "None of the available validators can be applied to the selected set of fields." : IRW)
    // If +link{warnOnInvalidFieldSelection} is true, and the user selects a combination of
    // field(s) and locator(s) that will not be supported by any
    // +link{availableTypes,specified rule type}, this warning string will be displayed to
    // the user in a dialog.
    // @group i18nMessages
    // @visibility rules
    //<
    invalidFieldSelectionWarning:"None of the available validators can be applied to the selected set of fields.",

    shouldShowFieldPicker : function () {
        if (this.showFieldPicker != null) return this.showFieldPicker;
        return this.validatorIsRule;
    },

    getField : function(fieldName, suppressWarning) {
        return this.dataSource ? this.dataSource.getField(fieldName)
            : isc.DataSource.getFieldFromDataSources(fieldName ,this.dataSources, suppressWarning);
    },

    // Client side pickList Data (ValueMap) for the field picker
    getFieldData : function () {
        if (this._fieldNames == null) {
            var dataSources = this.dataSources;
            if (dataSources != null) {
                this._fieldNames = isc.DataSource.getCombinedDataSourceFields(this.dataSources);

            } else if (this.dataSource) {
                var dsFields = isc.getKeys(this.dataSource.getFields());
                this._fieldNames = dsFields.duplicate();
            }
            this._fieldData= [];
            for (var i = 0; i < this._fieldNames.length; i++) {
                var name = this._fieldNames[i],
                    field = this.getField(name);

                this._fieldData[i] = {
                    name:name,
                    title:field.title || field.name,
                    type:field.type || "text"
                }
            }
        }
        return this._fieldData;
    },
    updateFieldName : function (fieldName, locator) {

        this.fieldName = fieldName;
        this.locator = locator;
        var hasFields = fieldName != null && (!isc.isAn.Array(fieldName) || fieldName.length > 0),
            hasLocators = locator != null && (!isc.isAn.Array(locator) || locator.length > 0),
            supportedTypes = (hasFields || hasLocators) ? this.getSupportedTypes(fieldName, locator)
                                                        : [],
            currentValidatorIsValid;

        if (this.validatorType != null && supportedTypes.length > 0) {
            currentValidatorIsValid = supportedTypes.contains(this.validatorType);
        }

        if (this.validatorForm) {
            if (!currentValidatorIsValid) {
                this.validatorForm.setValue("type", null);
                this.validatorType = null;
            }
            if (supportedTypes.length == 0) {
                this.validatorForm.setDisabled(true);
            } else {
                this.typePicker.setValueMap(supportedTypes);
                this.validatorForm.setDisabled(false);
            }
        }
        // (Re)Build the filter clause form items.
        // - required if we change validator type [may be entirely different set of value items]
        // - required if we change field [value items may be type-specific or show value map
        //   of all other fields, etc]

        if ((hasLocators || hasFields) && this.validatorType != null) {

            var needsRebuild = false;
            if (!hasLocators) {
                var oldFieldNames = this._lastFieldNames;
                if (oldFieldNames == null) {
                    needsRebuild = true;
                } else {
                    needsRebuild = true;
                    for (var i = 0; i < oldFieldNames.length; i++) {
                        var oldFieldName = oldFieldNames[i];
                        if (isc.isAn.Array(fieldName) ? (fieldName.contains(oldFieldName))
                                               : (fieldName == oldFieldName) )
                        {
                            needsRebuild = false;
                            break;
                        }
                    }

                }

            }
            this._lastFieldNames = (fieldName == null) ? null
                                    :   (isc.isAn.Array(fieldName) ? fieldName : [fieldName]);

            if (this.valuesForm.clause.getValue("operator") != this.validatorType) {
                needsRebuild = true;
            }


            if (isc.isAn.Array(fieldName)) fieldName = fieldName[0];
            this.valuesForm.fieldName = fieldName;

            this.valuesForm.clause.setValue("fieldName", fieldName);
            this.valuesForm.clause.setValue("operator", this.validatorType);

            if (needsRebuild) {

                // We can't just call 'fieldNameChanged()' - that'll attempt to compare the
                // operatorType with an operator object using 'DataSource.getSearchOperator()' which
                // doesn't apply outside of Criteria editing. Insted call updateValueItems directly.
                var validatorDefinition = this.getValidatorDefinition(this.validatorType);

                this.valuesForm.updateValueItems(
                        this.valuesForm.getField(fieldName), validatorDefinition, fieldName);
            }
        }
        this.updateValuesFormVisibility();
    },

    updateValuesFormVisibility : function () {

        if (this.valuesForm) {
            if ((this.fieldName == null && this.locator == null) || this.validatorType == null) {
                this.validatorForm.getItem("valuesItem").hide();
            } else {
                if (!this.valuesForm.isVisible()) {
                    this.validatorForm.getItem("valuesItem").show();
                }
            }
        }
    },

    //> @attr ruleEditor.triggerEventPicker (AutoChild FormItem : null : IR)
    // Field for picking +link{rule.triggerEvent}. This form item will only be visible if
    // the user is editing a rule (see +link{validatorIsRule}).
    // @visibility rules
    //<

    //> @attr ruleEditor.editStartEventTitle (string : "Edit start": IR)
    // User-friendly title for editStart event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    editStartEventTitle:"Edit start",

    //> @attr ruleEditor.editStartAndChangedEventTitle (string : "Edit start/changed": IR)
    // User-friendly title for editStartAndChanged event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    editStartAndChangedEventTitle:"Edit start/changed",

    //> @attr ruleEditor.editorEnterEventTitle (string : "Editor enter": IR)
    // User-friendly title for editorEnter event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    editorEnterEventTitle:"Editor enter",

    //> @attr ruleEditor.editorExitEventTitle (string : "Editor exit": IR)
    // User-friendly title for editorExit event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    editorExitEventTitle:"Editor exit",

    //> @attr ruleEditor.changedEventTitle (string : "Changed": IR)
    // User-friendly title for changed event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    changedEventTitle:"Changed",

    //> @attr ruleEditor.submitEventTitle (string : "Submit": IR)
    // User-friendly title for submit event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    submitEventTitle:"Submit",

    //> @attr ruleEditor.manualEventTitle (string : "Manual": IR)
    // User-friendly title for manual event shown in triggerEventPicker.
    // @group i18nMessages
    // @visibility rules
    //<
    manualEventTitle:"Manual",

    triggerEventPickerConstructor:"SelectItem",
    triggerEventPickerDefaults:{
        name:"event",
        startRow:true,
        changed : function (form,item,value) {
            this.creator.updateTriggerEvent(value);
        },
        showIf : function () {
            return this.creator.shouldShowTriggerEventPicker();
        }
    },

    shouldShowTriggerEventPicker : function () {
        if (this.showTriggerEventPicker != null) return this.showTriggerEventPicker;
        return this.validatorIsRule;
    },

    // ---
    // Conditional / applyWhen UI
    // ---

    // 'applyWhenForm' contains just the checkbox to show /hide the conditional criteria form
    applyWhenFormConstructor:"DynamicForm",
    applyWhenFormDefaults:{
        numCols:2,
        fixedColWidths:true,

        height:20
    },

    applyWhenItemDefaults:{
        showLabel:true, editorType:"CheckboxItem",
        width:20, showTitle:false, align:"right", vAlign:"top",
        changed:"this.form.creator.updateConditionalForm(value)",
        init:function() {
            // Simulate display as title with corresponding prefix/suffix handling
            if (this.form) {
                var form = this.form,
                    orient = form.getTitleOrientation(),
                    titlePrefix = (orient == "right" ? form.rightTitlePrefix : form.titlePrefix),
                    titleSuffix = (orient == "right" ? form.rightTitleSuffix : form.titleSuffix)
                ;
                this.title = titlePrefix + this.title + titleSuffix;
            }
            this.Super("init", arguments);
        }
    },

    //> @attr ruleEditor.filterTopOperatorAppearance (string : "radio" : IR)
    // Set the initial "If" section +link{FilterBuilder.topOperatorAppearance}. Note that
    // when an existing rule that has nested clauses in the <code>applyWhen</code> attribut
    // is edited by calling +link{setRule} the "If" section will be automatically switched
    // to the "bracket" setting.
    // @visibility rules
    //<
    filterTopOperatorAppearance:"radio",

    //> @attr ruleEditor.conditionalForm (AutoChild FilterBuilder : null : IR)
    // Automatically generated filter-builder used to edit the +link{rule.applyWhen} attribute
    // when editing a rule. Only visible if +link{ruleEditor.validatorIsRule} is true.
    // @visibility rules
    //<

    conditionalFormConstructor:"FilterBuilder",
    conditionalFormDefaults:{
        showFieldTitles:false,
        fieldPickerProperties: {
        },
        showModeSwitcher:true
    },

    createConditionalForm : function () {
        var topOperatorAppearance = this.filterTopOperatorAppearance || "radio";
        this.conditionalForm = this.createAutoChild("conditionalForm",
            {dataSource:this.dataSource, dataSources:this.dataSources,topOperatorAppearance:topOperatorAppearance}
        );
        this.conditionalForm.fieldPickerProperties.pickListWidth = this.conditionalForm.getWidth();
        return this.conditionalForm;
    },

    updateConditionalForm : function (show) {
        var placeholder = this.applyWhenForm.getItem("placeholder"),
            item = this.applyWhenForm.getItem("conditionalItem");
        if (!show) {
            placeholder.show();
            item.hide();
        } else {
            var criteria = this.applyWhen || {};
            this.conditionalForm.setCriteria(criteria);
            placeholder.hide();
            item.show();
        }
        // When setting/clearing a rule set the filter to the simplest
        // for applicable for the criteria.
        this.conditionalForm.setTopOperatorAppearance(isc.DataSource.isFlatCriteria(criteria) ? "radio" : "bracket");
    },

    // ---
    // Validator Config UI: type picker and valuesForm
    // ---

    // validatorForm - contains the 'typePicker' and the valuesForm CanvasItem
    validatorFormConstructor:"DynamicForm",
    validatorFormDefaults:{
        numCols:2,
        fixedColWidths:true,
        height:20
    },

    //> @attr ruleEditor.typePicker (AutoChild FormItem : null : IR)
    // Field for picking +link{validatorType}.
    // @visibility rules
    //<

    //> @attr ruleEditor.showTypePicker (boolean : null : IR)
    // Whether the +link{typePicker} is shown. If not explicitly specified, the typePicker will
    // be shown if +link{validatorType} is not specified at initialization time.
    // @visibility rules
    //<

    typePickerConstructor:"SelectItem",

    typePickerDefaults:{
        name:"type",
        width:"*",
        pickListProperties: { sortField: 0 },
        showIf:function () {
            var ruleEditor = this.form.creator;
            return ruleEditor.showTypePicker == false ? false : true;
        },
        getValueMap:function () {
            return this.creator.getTypeValueMap();
        },
        changed:function(form,item,value) {
            this.creator.updateValidatorType(value);
        }
    },

    // ValueMap for the validator type form item.
    // The available validator / rule types will vary depending on what the selected field
    // and locator is.
    getTypeValueMap : function () {
        return this.getSupportedTypes(this.fieldName, this.locator);
    },

    getSupportedTypes : function (fieldName, locators) {
        var types = this.availableTypes,
            supportedTypesValueMap = {};

        fieldName = fieldName || [];
        locators = locators || [];
        if (!isc.isAn.Array(fieldName)) fieldName = [fieldName];
        if (!isc.isAn.Array(locators)) locators = [locators];

        // if we have no selected fields/validators we won't show any options.
        if (types.length == 0 || fieldName.length == 0 && locators.length ==0) {
            return [];
        }


        var rangeType = null,
            validRangeTypes = {date:true, float:true, integer:true, time:true};

        for (var i = 0; i < types.length; i++) {
            var validator = this.getValidatorDefinition(types[i]) || {},
                showOption = true;

            if (locators.length > 0) {
                // only "rules" support locators.
                if (!validator.isRule) {
                    showOption = false;
                } else {
                    // AutoTest has a method to extract the target object type from an object
                    // locator (without having to resolve to a live object).
                    for (var ii = 0; ii < locators.length; ii++) {
                        // If the rule doesn't support the targetObjectType, skip it.
                        var targetObjectType = isc.AutoTest.getLocatorObjectType(locators[ii]),
                            validTargets = validator.supportedTargets || ["FormItem"];
                        if (!validTargets.contains(targetObjectType)) {
                            showOption = false;
                            break;
                        }
                    }
                }
            }

            // Our chosen locators may already disallow this validator type, so move onto the
            // next one.
            if (!showOption) continue;

            // For fields, validators are valid depending on data type.
            if (fieldName.length > 0) {
                for (var ii = 0; ii < fieldName.length; ii++) {
                    var field = this.getField(fieldName[ii]);
                    if (field == null) {
                        this.logWarn("unable to retrieve field for:" + fieldName[ii]);
                        continue;
                    }
                    var fieldDataType = isc.SimpleType.getBaseType(field.type || "text");
                    // Special-case range which maps to different validators depending on type
                    if (types[i] == "range") {
                        if (!validRangeTypes[fieldDataType] ||
                            (rangeType != null && rangeType != fieldDataType))
                        {
                            showOption = false;
                            // skip remaining fields
                            break;
                        } else {
                            // rangeType allows us to support only field type being chosen
                            // for ranges since more than one would imply we're generating
                            // multiple validators of different types.
                            rangeType = fieldDataType;
                        }
                    } else {
                        // dataType:"none" implies the validator doesn't care about the
                        // data-type of the target
                        if (validator.dataType != null && validator.dataType != "none"
                            && validator.dataType != fieldDataType) {
                            showOption = false;
                            // skip remaining fields
                            break;
                        }
                    }
                }
            }

            if (showOption) {
                var validatorDefinition = this.getValidatorDefinition(types[i]);
                var description = validatorDefinition.description || isc.DataSource.getAutoTitle(types[i]);
                supportedTypesValueMap[types[i]] = description;
            }
        }
        return supportedTypesValueMap;

    },

    // This method fired when the validator type changes.
    // Refreshes the valuesForm
    updateValidatorType : function (type, forceRebuild) {
        if (this.validatorType == type && !forceRebuild) return;
        this.validatorType = type;
        if (type != null) {
            var currentValuesForm = this.valuesForm,
                newValuesForm = this.getValuesForm(type);
            // Note that 'getValuesForm()' will actually update the valuesForm's valueItems

            if (currentValuesForm != newValuesForm) {
                this.valuesForm = newValuesForm;
                this.validatorForm.getItem("valuesItem").setCanvas(this.valuesForm);
                // (Don't destroy old validator form - we may want to reuse it
            }
        }
        // This'll actually hide the form if there's no selected validatorType
        this.updateValuesFormVisibility();
    },

    messageFormConstructor:"DynamicForm",
    messageFormDefaults:{
        numCols:2,
        width:"100%",
        height:20
    },

    //> @attr ruleEditor.errorMessageItem (TextItem AutoChild : {...} :IR)
    // Item for editing the +link{validator.errorMessage,errorMessage} of the rule being edited. Displayed
    // in the +link{ruleEditor.messageForm}. Implemented as an autoChild, so may be customized
    // via <code>errorMessageItemProperties</code>.
    // @visibility rules
    //<
    errorMessageItemDefaults:{
        editorType:"TextItem",
        width:"*"
    },

    //> @attr ruleEditor.valuesForm (AutoChild FilterClause : null : IR)
    // Form used for editing the attributes of a validator.
    // @visibility rules
    //<
    // This is a customized filterClause -- we use the class so it will derive the appropriate
    // form items to show based on available dataSource fields, field.type and validator.valueType
    // but we make the following fundamental changes:
    // - suppress the "remove" icon
    // - suppress the "fieldPicker" field (shown directly in the RuleEditor instead)
    // - suppress the "operator" picker. The clause will be passed validator definition objects
    //   instead of criterion operator objects. We show an operator picker directly in the RuleEditor
    // - never call the standard 'getCriterion' method - we're building validators, not criteria.
    //   Instead we duplicate the relevant bits of this to extract the values from the value field(s)
    //   and for custom editors, call the special validator.getAttributesFromEditor() API

    valuesFormConstructor:"FilterClause",

    valuesFormDefaults:{
        // validatorAttribute / rangeStart/end attributes and getAttributesFromEditor may be
        // defined on the validator definitions.
        customGetValuesFunction:"getAttributesFromEditor",
        customSetValuesFunction:"setEditorAttributes",
        operatorAttribute:"type",

        // Don't show the field-picker item
        fieldPickerProperties:{
            showIf:"return false"
        },

        getEditorType : function (field, validatorType) {
            var validatorDefinition = this.creator.getValidatorDefinition(validatorType);
            if (validatorDefinition && validatorDefinition.valueType == "custom" &&
                validatorDefinition.editorType)
            {
                return validatorDefinition.editorType;
            }
            if (field && isc.SimpleType.inheritsFrom(field.type, "date")) return "RelativeDateItem";
            if (validatorType == "readOnly") {
                return "ReadOnlyRuleEditor";
            }
            // Return null - this'll back off to default behavior
            return null;
        }

    },

    // Helper to convert the "validatorType" understood by this widget
    // to the validatorType supported at the validator level.
    // This basically resolves "range" to "dateRange" / "integerRange" etc based
    // on field type.
    resolveValidatorType : function (type) {

        if (type == null) type = this.validatorType;
        if (type == null) return null;

        // special-case "range" - get the range for the field type
        if (type == "range") {
            var field = this.fieldName,
                fieldType;
            if (field != null) {
                if (!isc.isAn.Array(field)) field = [field];
                var typeMismatch = false;
                for (var i = 0; i < field.length; i++) {
                    var fieldObj = this.getField(field[i]),

                        currentFieldType = fieldObj.type || "integer";
                    // Resolve to base type (so a custom subtype of "integer" still uses
                    // an integerRange, say)
                    currentFieldType = isc.SimpleType.getBaseType(currentFieldType);

                    if (fieldType == null) {
                        fieldType = currentFieldType
                    } else {
                        if (currentFieldType != fieldType &&
                            // Special case date vs datetime (same range validator)
                            (currentFieldType != "date" && fieldType != "datetime" &&
                             currentFieldType != "datetime" && fieldType != "date"))
                        {
                            typeMismatch = true;
                            fieldType = "integer";
                        }
                    }
                }
                if (typeMismatch) {
                    this.logWarn("'range' validator for fields with differing types:"
                         + this.echo(this.fieldName) +
                         ". Defaulting to integer type data", "RuleEditor");
                } else {
                    this.logDebug("'range' validator for field[s]:"
                         + this.echo(this.fieldName) +
                         ". Assuming " + fieldType + " type data", "RuleEditor");
                 }

            // no field at all? Default to integer

            } else {
                this.logInfo("Attempting to get 'range' validator with no field type - defaulting to integer",
                    "RuleEditor");
                fieldType = "integer";
            }
                // IF we don't have a field, this is sorta invalid, but default to integerRange

            // All ranges:
            // integerRange
            // dateRange
            // timeRange
            // floatRange
            // - default to integerRange if its none of these!
            // ('lengthRange' is the only range that makes sense for strings, but it'd be
            // an odd behavior if the user picks just "range" on a string field).
            if (fieldType == "date" || fieldType == "datetime") {
                type = "dateRange";
            } else if (fieldType == "time") {
                type = "timeRange";
            } else if (fieldType == "float") {
                type = "floatRange"
            } else {
                type = "integerRange"
            }

        }
        return type;
    },

    // Helper to get a 'validatorDefinition' from a validatorType name
    getValidatorDefinition : function (type) {
        type = this.resolveValidatorType(type);
        return isc.Validator._validatorDefinitions[type];
    },


    getValuesForm : function (validatorType) {

        if (validatorType != null) {
            var validatorDefinition = this.getValidatorDefinition(validatorType),
                valueType = validatorDefinition.valueType;
            validatorDefinition.ID = validatorType;
        }

        var fieldName = this.fieldName;

        if (isc.isAn.Array(fieldName)) fieldName = fieldName[0];
        if (this.valuesForm) {
            var field = fieldName ? this.valuesForm.getField(fieldName) : null;
            this.valuesForm.updateValueItems(field, validatorDefinition, fieldName);

            this.valuesForm.clause.setValue("operator", validatorType);

            return this.valuesForm;
        } else {

            var form = this.valuesForm = this.createAutoChild("valuesForm", {
                visibility:([this.fieldName || this.locator] ? "inherit" : "hidden"),
                showRemoveButton:false,
                // support multiple or singular dataSource
                dataSources:this.dataSources,
                dataSource:this.dataSource,
                fieldName:fieldName,
                operatorType:validatorType
            });

            // hide the operatorPicker in the clause - we have a separate item for this.

            var clauseForm = form.clause;
            clauseForm.getItem("operator").hide();
            // allow unknown values so we can set to 'validatorTypes' that aren't present in the
            // standard 'operators' valueMap

            clauseForm.getItem("operator").addUnknownValues = true;
            return form;
        }
    },

    // -----
    // End of UI
    // -----

    //> @method ruleEditor.setValidatorType()
    // Update the +link{ruleEditor.validatorType}
    // @param type (ValidatorType) validatorType
    // @visibility rules
    //<
    setValidatorType : function (type) {
        this.validatorForm.setValue("type", type);
        this.updateValidatorType(type);
    },

    //> @method ruleEditor.setFieldName()
    // Sets the fieldName applied to the rule.
    // @visibility rules
    //<
    // For validators managed by a rulesEngine,
    // rule.fieldName specifies what field the validator is attached to.
    // For normal forms the validators are defined as an attribute on the field.
    // We need to know the fieldName in order to show the correct UI - assume the calling code
    // will set this at init time or runtime.
    setFieldName : function (fieldName) {
        var locator = this.locator;
        if (this.fieldPicker) {
            var combinedValue = fieldName
            if (locator != null) {
                if (!isc.isAn.Array(locator)) combinedValue = [locator];
                else combinedValue = locator.duplicate();
                if (isc.isAn.Array(fieldName)) {
                    combinedValue.addList(fieldName);
                } else if (fieldName != null) {
                    combinedValue.add(fieldName);
                }
            }
            // fieldPicker displays both field and locator.
            this.fieldPicker.setValue(combinedValue);
        }
        this.updateFieldName(fieldName, this.locator);
    },

    //> @method ruleEditor.setLocator()
    // Sets the locator applied to the rule.
    // @visibility rules
    //<
    setLocator : function (locator) {
        if (this.fieldPicker) {
            var fieldName = this.fieldName,
                combinedValue = locator;
            if (fieldName != null) {
                if (!isc.isAn.Array(fieldName)) combinedValue = [fieldName];
                else combinedValue = fieldName.duplicate();
                if (isc.isAn.Array(locator)) {
                    combinedValue.addList(locator);
                } else if (locator != null) {
                    combinedValue.add(locator);
                }
            }
            // fieldPicker displays both field and locator.
            this.fieldPicker.setValue(combinedValue);
        }
        this.updateFieldName(this.fieldName, locator);
    },

    //> @method ruleEditor.setTriggerEvent()
    // Sets the +link{triggerEvent} for this ruleEditor
    // @param event (TriggerEvent) new trigger event
    // @visibility rules
    //<
    setTriggerEvent : function (event) {
        if (this.triggerEventPicker) {
            this.triggerEventPicker.setValue(event);
        }
        this.updateTriggerEvent(event);
    },
    updateTriggerEvent : function (event) {
        this.triggerEvent = event;
    },


    //> @method ruleEditor.setApplyWhen()
    // Sets the +link{applyWhen} attribute for this ruleEditor.
    // @param applyWhen (AdvancedCriteria) criteria indicating when the rule should be applied.
    // @visibility rules
    //<
    setApplyWhen : function (criteria) {
        this.applyWhen = criteria;
        this.applyWhenForm.setValue("applyWhen", (this.applyWhen != null));
        this.updateConditionalForm(criteria != null);
    },

    getApplyWhen : function () {
        if (this.applyWhenForm.getValue("applyWhen")) {
            this.applyWhen = this.conditionalForm.getCriteria();
        } else {
            this.applyWhen = null;
        }
        return this.applyWhen;
    },

    // attributes from the 'valuesForm'.
    // Typically this is just the single value/fieldName, but may include other fields
    // depending on the valueType / editorType etc of the validator.

    getAttributesFromClause : function () {
        var baseDef = this.getValidatorDefinition();
        var fieldName = this.fieldName,
            validatorClause = this.valuesForm;

        if (isc.isAn.Array(fieldName)) fieldName = fieldName[0];
        var validatorAttributes = validatorClause.getClauseValues(fieldName, baseDef);
        return validatorAttributes;
    },

    setClauseAttributes : function (attributes) {
        if (this.valuesForm == null) return;
        // update the "value" field[s] of the clause form
        // That's typically "value" or "start"/"end" but might call custom setter for some
        // validator types.
        // Note that this sill not update validatorType/fieldName -- that should already have
        // been handled via setRule() if necessary.
        var baseDef = this.getValidatorDefinition();
        var fieldName = this.fieldName;

        if (isc.isAn.Array(fieldName)) fieldName = fieldName[0];

        this.valuesForm.setClauseValues(fieldName, baseDef, attributes);

    },

    //> @method ruleEditor.getValidator()
    // synonym for +link{getRule()}.
    // @return (Validator) edited validator object
    // @visibility rules
    //<
    getValidator : function () {
        return this.getRule();
    },

    //> @method ruleEditor.getRule()
    // Get the rule (validator). Will return null if +link{fieldName} or +link{validatorType} are
    // not set.
    // @return (Validator) edited validator object
    // @visibility rules
    //<
    getRule : function () {
        if (this.validatorType == null || (this.fieldName == null && this.locator == null)) return null;
        var validator = {};
        // resolveValidatorType will convert "range" to "dateRange" (etc) based on field type.
        validator.type = this.resolveValidatorType(this.validatorType);

        if (this.nameForm != null) {
            var name = this.nameForm.getValue("name");
            if (name != null) validator.name = name;
            var description = this.nameForm.getValue("description");
            if (description != null) validator.description = description;
        }

        // attributes from the filterClause form
        if (this.valuesForm != null) {
            var validatorAttributes = this.getAttributesFromClause();
            for (var attr in validatorAttributes) {
                // Don't clobber the "type" - we already resolved that to a meaningful
                // validatorType
                if (attr == "type") continue;

                validator[attr] = validatorAttributes[attr];
            }
        }


        if (this.validatorIsRule) {
            if (this.fieldName) validator.fieldName = this.fieldName;
            if (this.locator) validator.locator = this.locator;

            if (this.triggerEvent) validator.triggerEvent = this.triggerEvent;
        } else {
            delete validator.fieldName;
        }

        validator.errorMessage = this.messageForm.getValue("errorMessage");

        // applyWhen criteria for the validator
        var applyWhen = this.getApplyWhen();
        if (applyWhen != null) validator.applyWhen = applyWhen;

        // If description is not shown in the editor, create a description
        // based on the entered values.
        if (!this.showNameForm) {
            validator.description = this.createRuleDescription(validator);
        }
        return validator;
    },



    createRuleDescription : function (rule) {
        if (!rule.type) return null;

        var validatorDefinition = this.getValidatorDefinition(rule.type),
            title = validatorDefinition.title || isc.DataSource.getAutoTitle(rule.type),
            eventValueMap = this.triggerEventPicker.valueMap,
            description = title + " for field " + rule.fieldName + " on " + eventValueMap[rule.triggerEvent]
        ;

        if (rule.applyWhen) {
            description += " if " + isc.DataSource.getAdvancedCriteriaDescription(rule.applyWhen, this.dataSources || this.dataSource);
        }

        return description;
    },

    //> @method ruleEditor.validate()
    // Validate the current set of values for the rule.
    // @return (boolean) true if validation passed for all component forms, false otherwise.
    // @visibility rules
    //<

    validate : function () {
        var failed = false;
        // if name/description have been marked as required, enforce this
        if (this.nameForm) failed = this.nameForm.validate() == false;
        if (this.mainForm) failed = (this.mainForm.validate() == false) || failed;
        if (this.applyWhenForm && this.applyWhenForm.getValue("applyWhen")) {
            failed = (this.conditionalForm.validate() == false) || failed;
        }
        if (this.validatorForm) {
            failed = (this.validatorForm.validate() == false) || failed;
            if (this.valuesForm) failed = (this.valuesForm.validate() == false) || failed;
        }
        if (this.messageForm) failed = (this.messageForm.validate() == false) || failed;
        return !failed;
    },

    //> @method ruleEditor.setRule()
    // Show the specified rule in this ruleEditor
    // @param rule (Validator) Rule to edit.
    // @visibility rules
    //<
    // initTime param used internally
    setRule : function (rule, initTime) {

        this.validator = this.rule = rule;

        if (initTime) {
            this.validatorType = rule.type;
            this.applyWhen = rule.applyWhen;
            this.fieldName = rule.fieldName;
            this.locator = rule.locator;
            this.triggerEvent = rule.triggerEvent;
            // errorMessage is applied lazily to the messageForm when its initialized.
        } else {
            if (this.nameForm) {
                this.nameForm.setValue("name", rule.name);
                this.nameForm.setValue("description", rule.description);
            }

            if (rule.fieldName != null || rule.locator != null) {
                // we have to call both methods even if the property is null,
                // as we need to clear existing fieldName / locator
                // as well as apply new values
                this.setFieldName(rule.fieldName);
                this.setLocator(rule.locator);
            }

            this.setTriggerEvent(rule.triggerEvent);
            this.setValidatorType(rule.type);
            this.setApplyWhen(rule.applyWhen);
            this.messageForm.setValue("errorMessage", rule.errorMessage);

            this.setClauseAttributes(rule);
        }
    },
    //> @method ruleEditor.setValidator()
    // Show the specified validator in this ruleEditor. Synonym for setRule().
    // @param rule (Validator) Rule to edit.
    // @visibility rules
    //<
    setValidator : function (validator) {

        this.setRule(validator);
    },

    //> @method ruleEditor.clearRule()
    // Clear the ruleEditor's values (dropping the current rule entirely). Note that this will
    // clear all settings for the rule, including <code>fieldName</code>.
    // @visibility rules
    //<
    clearRule : function () {
        this.rule = this.validator = null;
        this.setFieldName(null);
        this.setValidatorType(null);
        this.setApplyWhen(null);
        this.setTriggerEvent(null);
        if (this.nameForm) this.nameForm.clearValues();
        if (this.messageForm) this.messageForm.clearValue("errorMessage");
    },

    //> @method ruleEditor.clearValidator()
    // Clear the ruleEditor's values (dropping the current validator entirely).
    // @visibility rules
    //<
    clearValidator : function () {
        return this.clearRule();
    }

});


if (isc.DynamicForm) {


// Custom form item types for editing built-in validator definition objects
// These are referred to via the "validator.editorType" attribute


isc.defineClass("SubstringCountEditor", "CanvasItem").addProperties({

    canvasConstructor:"DynamicForm",
    canvasDefaults:{
        numCols:3
    },

    substringFieldDefaults:{
        name:"substring",
        showTitle:false, type:"text", colSpan:"*", width:"*"
    },
    countFieldDefaults:{
        name:"count", showTitle:false, hint:"Count", showHintInField:true,
        width:50, type:"integer"
    },
    operatorFieldDefaults:{
        name:"operator", title:"Operator", editorType:"SelectItem",
        width:50,
        defaultValue:"==", allowEmptyValue:false,
        valueMap:["==", "!=", "<", "<=", ">", ">=" ]
    },
    createCanvas : function (form,item) {

        var substringField = isc.addProperties({},
                this.substringFieldDefaults, this.substringFieldProperties),
            countField = isc.addProperties({},
                this.countFieldDefaults, this.countFieldProperties),
            operatorField = isc.addProperties({},
                this.operatorFieldDefaults, this.operatorFieldProperties);

        return this.canvas = this.createAutoChild(
            "canvas",
            { items:[
                    substringField,
                    countField,
                    operatorField
                ]
            }
        );
    }
});

isc.defineClass("FloatRangeEditor", "CanvasItem").addProperties({

    canvasConstructor:"DynamicForm",
    canvasDefaults:{
        numCols:2
    },
    minFieldDefaults:{
        name:"min",
        showTitle:false, type:"float",
        hint:"Min", showHintInField:true
    },
    maxFieldDefaults:{
        name:"max",
        showTitle:false, type:"float",
        hint:"Max", showHintInField:true
    },
    exclusiveFieldDefaults:{
        name:"exclusive", title:"Exclusive",
        colSpan:"*",
        prompt:"Range is exclusive (does not include min/max values)",
        type:"boolean",
        editorType:"CheckboxItem", defaultValue:false
    },
    createCanvas : function (form,item) {

        var minField = isc.addProperties({},
                 this.minFieldDefaults, this.minFieldProperties),
            maxField = isc.addProperties({},
                this.maxFieldDefaults, this.maxFieldProperties),
            exclusiveField = isc.addProperties({},
                this.exclusiveFieldDefaults, this.exclusiveFieldProperties);

        return this.canvas = this.createAutoChild(
            "canvas",
            { items:[
                    minField,
                    maxField,
                    exclusiveField
                ]
            }
        );
    }
});

isc.defineClass("FloatPrecisionEditor", "CanvasItem").addProperties({

    canvasConstructor:"DynamicForm",
    canvasDefaults:{
        numCols:1
    },

    precisionFieldDefaults:{
        name:"precision",
        showTitle:false, type:"float",
        hint:"Precision", showHintInField:true
    },
    roundFieldDefaults:{
        showTitle:false,
        name:"roundToPrecision", title:"Round to precision",
        type:"boolean",
        editorType:"CheckboxItem", defaultValue:false
    },
    createCanvas : function (form,item) {

        var precisionField = isc.addProperties({},
                this.precisionFieldDefaults, this.precisionFieldProperties),
            roundField = isc.addProperties({},
                this.roundFieldDefaults, this.roundFieldProperties);

        return this.canvas = this.createAutoChild(
            "canvas",
            { items:[
                    precisionField, roundField
                ]
            }
        );
    }
});

isc.defineClass("MaskRuleEditor", "CanvasItem").addProperties({
    // Needs 2 strings - mask (a regex), and transformTo
    canvasConstructor:"DynamicForm",
    canvasDefaults:{
        numCols:1
    },

    maskFieldDefaults:{
        name:"mask", editorType:"TextItem",
        showTitle:false,
        hint:"mask", showHintInField:true
    },
    transformFieldDefaults:{
        name:"transformTo", editorType:"TextItem",
        showTitle:false,
        hint:"transformTo", showHintInField:true
    },
    createCanvas : function (form,item) {

        var maskField = isc.addProperties({},
                this.maskFieldDefaults, this.maskFieldProperties),
            transformField = isc.addProperties({},
                this.transformFieldDefaults, this.transformFieldProperties);

        return this.canvas = this.createAutoChild(
            "canvas",
            { items:[
                    maskField, transformField
                ]
            }
        );
    }
});

isc.defineClass("PopulateRuleEditor", "BlurbItem").addProperties({

    emptyFormulaText:"Click the icon to select a formula",
    formulaVarsTitle:"Formula Variables:",
    formulaTitle:"Formula:",
    editFormulaPrompt:"Click to edit formula",

    formatValue:function (value,record,form,item) {
        // rule is an object containing "formula" (string) and "formulaVars" (map)
        if (value == null || value.formula == null) {
            return this.emptyFormulaText;
        }
        var formulaVars = value.formulaVars,
            keys = isc.getKeys(formulaVars).sort(),
            variables = ""
        ;
        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            variables += key +":&nbsp;" + formulaVars[key] + "\n";
        }

        return "<table class=" + this.getTextBoxStyle() + "><tr><td>" + this.formulaVarsTitle + "</td><td>"
                + variables + "</td></tr>" +
                "<tr><td>" + this.formulaTitle + "</td><td>" + value.formula + "</td></tr></table>";
    },
    icons:[
        {click:"item.showFormulaWindow()"}
    ],

    init:function () {
        this.icons[0].prompt = this.editFormulaPrompt;
        return this.Super("init", arguments);
    },

    formulaWindowConstructor:"Window",
    formulaWindowDefaults:{
        title: "Formula Editor",
        showMinimizeButton: false, showMaximizeButton: false,
        isModal: true,
        showModalMask:true,
        autoSize: true,
        autoCenter: true,
        autoDraw: true,
        headerIconProperties: { padding: 1,
            src: "[SKINIMG]ListGrid/formula_menuItem.png"
        },

        closeClick: function () {
            // call the method to cancel editing on the formulaBuilder. That'll automatically
            // dismiss this window.
            this.items.get(0).completeEditing(true);
        }

    },

    formulaBuilderConstructor:"FormulaBuilder",
    formulaBuilderDefaults:{
        width:300,
        // FormulaBuilders typically edit formula fields for components - but we just want UI
        // for creating and testing formulae.
        // Hide any UI to do with formula fields / source component fields
        showTitleField:false,
        showAutoHideCheckBox:false,

        // no need for "save and add another"! We'll use save/cancel
        // (since we show the thing in a popup)
        showSaveAddAnotherButton:false,

        fireOnClose:function () {
            this.creator.userEditComplete(!this.cancelled);
        }
    },

    showFormulaWindow : function () {
        if (this.formulaBuilder == null) {
            this.formulaBuilder = this.createAutoChild(
                "formulaBuilder",

                {dataSource:this.form.creator.dataSource, dataSources:this.form.creator.dataSources,
                 mathFunctions: isc.MathFunction.getDefaultFunctionNames()}
            );

            this.formulaWindow = this.createAutoChild("formulaWindow", {items:[this.formulaBuilder]});
        }
        // Clear the current value in the window if there is one.

        this.formulaBuilder.setValue("");
        this.formulaWindow.show();
    },

    userEditComplete : function (saveValue) {
        if (saveValue) {
            var formulaObj = this.formulaBuilder.getBasicValueObject(),
                formula,
                formulaVars;
            if (formulaObj != null) {
                formula = formulaObj.text;
                formulaVars = formulaObj.formulaVars;
            }
            if (formula != null) {
                this.storeValue({formula:formula, formulaVars:formulaVars});
            } else {
                this.storeValue(null);
            }
            this.redraw();
        }
        this.formulaWindow.clear();
    }

});

isc.defineClass("ReadOnlyRuleEditor", "SelectItem").addProperties({
    defaultValue:isc.Validator.READONLY,
    valueMap:[
        isc.Validator.HIDDEN,
        isc.Validator.DISABLED,
        isc.Validator.READONLY
    ]
});

}   // End of check for DynamicForm being defined





//> @groupDef accessibility
// SmartClient is a fully accessible technology which fulfills the Section 508 requirements of
// U.S. government law and similar international standards.  Specificallly:
// <ul>
// <li> components are fully keyboard navigable and the browser's native focus indicator reveals
// keyboard focus to the user
// <li> components are themable/brandable, allowing a variety of high contrast and limited color
// range look and feel options to compensate for visual acuity disabilities
// <li> the WAI-ARIA standard is supported for adding semantic markup to components to identify them to
// screen readers such as NVDA or JAWS.
// </ul>
// <P>
// <b>WAI-ARIA support</b>
// <P>
// ARIA is a standard from the WAI (Web Accessibility Institute) that allows modern Ajax applications to
// add semantic markup to the HTML used to create modern Ajax interfaces to enable screen reader support.
// This semantic markup allows a screen reader to identify the function and state of complex components
// such as load-on-demand lists and trees even though they are composed of simple elements such a &lt;div&gt;s.
// <P>
// Note that ARIA support is the correct way to evaluate the accessibility of a web
// <i>application</i>.  Standards which apply to a web <i>site</i>, such as ensuring that all interactive
// elements are composed of native HTML anchor (&lt;a&gt;) or &lt;form&gt; controls, cannot and should
// not be applied to a web <i>application</i>.  A web application's accessibility must be evaluated in
// terms of its ARIA support.
// <P>
// By default, SmartClient components will write out limited ARIA markup sufficient to navigate basic
// menus and buttons.  Full screen reader mode is not enabled by defaut because it has a small
// performance impact and subtly changes the management of keyboard focus in a way that is slightly worse
// for unimpaired users.
// <P>
// The limited ARIA support which is enabled by default is intended to allow a screen reader user to
// navigate to a menu to enable full screen reader support.  This is analogous to a partially visually
// impaired user ariving at a site with normal theming and needing to switch to a high-contrast skin.
// <P>
// To enable full screenReader mode, call +link{isc.setScreenReaderMode} before any
// SmartClient components are created or drawn.  This implies that if an end user dynamically enables
// full screen reader support, the application page must be reloaded, as an any existing components will
// not have full ARIA markup.
// <P>
// For an overview of ARIA, see +externalLink{http://www.w3.org/WAI/intro/aria.php}.
// <P>
// To completely disable ARIA markup, call
// +link{isc.setScreenReaderMode(),isc.setScreenReaderMode(false)} before any components are drawn.
// <P>
// <b>Recommended Screen Reader Configuration</b>
// <P>
// The recommended configuration for screen reader use is the most recent available release of Firefox
// and either the JAWS or NVDA screen reader.
// <P>
// While WAI-ARIA markup is provided for other browsers, support for WAI-ARIA itself is known to be
// limited in current release versions of IE and other browsers supported by SmartClient.
// <P>
// <b>Application-level concerns</b>
// <P>
// While SmartClient enables accessible web applications to be created, it is always possible for an
// application to violate accessibility standards.  The following is a brief and not exhaustive list of
// concerns for application authors:
// <ul>
// <li> for any operation that can be triggered via drag and drop, you should offer an equivalent
// keyboard-only means of performing the same operation.  For common grid to grid drags, this is easily
// accomplished using +link{ListGrid.transferSelectedData()}.
// <li> if you use a component in a way that is not typical, such as using an ImgButton as a
// non-interactive stateful display, set its +link{canvas.ariaRole} appropriately.  For a list of ARIA
// roles, see +externalLink{http://www.w3.org/WAI/PF/aria/roles#role_definitions}.
// Note that in most cases you will not need to modify the default ariaRole written out by
// the SmartClient framework with screenReader mode enabled.
// <li> for plain HTML content that is incorporated into an Ajax interface (such as an embedded help
// system), embed the HTML into an +link{HTMLFlow} (whose default ARIA role is "article") and ensure the
// HTML itself is accessible (for example, has "alt" attributes on all images which are semantically
// meaningful)
// <li> in addition to setting explicit ARIA roles per canvas, SmartClient also allows
// developers to specify values for explicit
// <smartclient>+link{canvas.ariaState,ARIA states}</smartclient>
// <smartgwt>+link{canvas.setAriaState(),ARIA states}</smartgwt>
// (see +externalLink{http://www.w3.org/TR/wai-aria/states_and_properties}) to be written
// out with the HTML for a component. <br>
// Note that, as with ariaRoles, in most cases the
// framework automatically writes out any appropriate aria state information based
// on the component being generated - you'd only make use of this property if
// using components in some custom way.
// To provide a concrete example: a developer might implement a logical nested
// "menu" built from a set of Button instances. In that case, some button might have
// ariaRole set to <code>"menuitem"</code> and (if it launches a sub-menu),
// also the +externalLink{http://www.w3.org/TR/wai-aria/states_and_properties#aria-haspopup,"haspopup"}
// aria state. The code for this would be something like:
// <smartclient>
// <pre>
// isc.Button.create({
//      // ... various properties
//
//      ariaRole:"menuitem",
//      ariaState:{haspopup:true}
// });
// </pre>
// </smartclient>
// <smartgwt>
// <pre>
//  myButton.setAriaRole("menuitem");
//  myButton.setAriaState("haspopup", true);
// </pre>
// </smartgwt>
// </ul>
// <p>
// <b>Known Screen Reader bugs / quirks</b>
// <p>
// JAWS: By default, JAWS treats a web page as a web document - text interspersed with graphics,
// links, etc. - and not as an application consisting of form controls, interactive buttons,
// lists, and so on. To enable application mode in JAWS, it is necessary to add <code>role="application"</code>
// to the &lt;body&gt; tag. See
// +externalLink{http://www.freedomscientific.com/Support/TechnicalSupport/Bulletin/1404,Freedom Scientific Bulletin 1404 - In ARIA&#44; what is the difference in how JAWS treats role="application" and role="document"?}
//
// @treeLocation Concepts
// @title Accessibility / Section 508 compliance
// @visibility external
//<

//> @classMethod isc.setScreenReaderMode()
// Enables full screen reader mode.  Must be called before any components are created.  See
// +link{group:accessibility}.
// @param newState (boolean) new setting
// @visibility external
//<
isc.setScreenReaderMode = function (newState) {
   isc.screenReader = newState;
}

//> @attr canvas.ariaRole (String : null : IR)
// ARIA role of this component.  Usually does not need to be manually set - see
// +link{group:accessibility}.
// @group accessibility
// @visibility external
//<

//> @attr canvas.ariaState (Object : null : IRA)
// ARIA state mappings for this component. Usually this does not need to be manually
// set - see +link{group:accessibility}.
// <P>
// This attribute should be set to a mapping of aria state-names to values - for example
// to have the "aria-haspopup" property be present with a value "true", you'd specify:
// <pre>
//  { haspopup : true }
// </pre>
// @group accessibility
// @visibility external
//<

//isc.screenReader = undefined; // initially undefined

// liteAria
// - may be explicitly set to true to minimize what Aria behaviors are enabled
// - otherwise we default to true in IE8 and earlier unless 'setScreenReaderMode(true)' has been
//   explicitly called.
isc.liteAria = null;

// internal DOM manipulation methods, don't document
isc.Canvas.addClassMethods({


    // this just indicates whether we write out basic ARIA attributes for most elements, not whether we
    // are in full screenReader mode
    ariaEnabled : function () {
        return isc.screenReader || isc.screenReader !== false &&
            ((isc.Browser.isIE && isc.Browser.version >=8) || !isc.Browser.isIE);
    },

    useLiteAria : function () {
        // allow "liteAria" to be explicitly specified.
        // Otherwise if screenReader is explicitly set to true, don't use liteAria
        // - otherwise use liteAria for <= IE8 since it's slow.
        if (isc.liteAria != null) return isc.liteAria;
        if (isc.screenReader == true) return false;
        return (isc.Browser.isIE && isc.Browser.version < 9);
    },

    setAriaRole : function (element, role) {
        if (!element) return;
        if (this.logIsDebugEnabled("aria")) {
            this.logDebug("ARIA role changed to: " + role +
                          " on element: " + this.echoLeaf(element), "aria");
        }
        element.setAttribute("role", role);
    },

    setAriaState : function (element, stateName, stateValue) {
        if (!element) return;
        if (this.logIsInfoEnabled("aria")) {
            this.logInfo("ARIA state: " + stateName + ": " + stateValue +
                         ", set on element: " + isc.echoLeaf(element), "aria");
        }

        element.setAttribute("aria-" + stateName, stateValue);
    },

    setAriaStates : function (element, state) {
        if (!element) return;
        if (state == null) return;
        for (var stateName in state) {
            this.setAriaState(element, stateName, state[stateName]);
        }
    },

    clearAriaState : function (element, stateName) {
        if (!element) return;
        element.removeAttribute("aria-" + stateName);
    },

    getAriaStateAttributes : function (ariaState) {
        var output = "";
        if (ariaState) {
            for (var stateName in ariaState) {
                var stateValue = ariaState[stateName];
                output += " aria-" + stateName + "='" + String.asAttValue(stateValue) + "'";
            }
        }
        return output;
    }
});

isc.Canvas.addMethods({
    // instance-level methods.  Canvases set ariaRole and ariaState on their
    // clipHandle

    //> @method canvas.setAriaRole()
    // Update the +link{canvas.ariaRole} at runtime
    // @param role (String) new ariaRole
    // @group accessibility
    // @visibility internal
    //<

    setAriaRole : function (role) {
        isc.Canvas.setAriaRole(this.getClipHandle(), role);
    },

    //> @method canvas.setAriaState()
    // Set a specific ARIA state for this component.
    //
    // @param stateName (String) aria state to update
    // @param stateValue (String | Boolean | Integer | Float) value for the aria state
    // @group accessibility
    // @visibility external
    //<
    setAriaState : function (stateName, stateValue) {
        isc.Canvas.setAriaState(this.getClipHandle(), stateName, stateValue);
    },
    setAriaStates : function (state) {
        isc.Canvas.setAriaStates(this.getClipHandle(), state);
    },
    clearAriaState : function (stateName) {
        isc.Canvas.clearAriaState(this.getClipHandle(), stateName);
    },

    // called during initial draw for non-IE browsers
    getAriaStateAttributes : function () {
        return isc.Canvas.getAriaStateAttributes(this.ariaState);
    }
});


if (isc.DynamicForm) {

isc.DynamicForm.addProperties({
    rightTitlePrefix: "<span aria-hidden='true'>:&nbsp;</span>",
    titleSuffix: "<span aria-hidden='true'>&nbsp;:</span>",
    requiredRightTitlePrefix: "<b><span aria-hidden='true'>:&nbsp;</span>",
    requiredTitleSuffix: "<span aria-hidden='true'>&nbsp;:</span></b>"
});

// General support for formItems
// ---------------------------------------------------------------------------------------
// Note: FormItemIcon: in FormItem.js, given fixed role="button" and icon.prompt made into aria-label.

isc.FormItem.addMethods({

    //> @attr formItem.ariaRole (String : null : IRWA)
    // ARIA role of this formItem.  Usually does not need to be manually set - see
    // +link{group:accessibility}.
    // @group accessibility
    // @visibility external
    //<

    //> @attr formItem.ariaState (Object : null : IRWA)
    // ARIA state mappings for this formItem. Usually this does not need to be manually
    // set - see +link{group:accessibility}.
    // <P>
    // This attribute should be set to a mapping of aria state-names to values - for example
    // to have the "aria-multiline" property be present with a value "true", you'd specify:
    // <pre>
    //  { multiline : true }
    // </pre>
    // @group accessibility
    // @visibility external
    //<

    // FormItems set ariaRole and ariaState on their focus element, if any

    //> @method formItem.setAriaRole()
    // Sets the ARIA role of this FormItem.  Usually does not need to be manually set - see
    // +link{groupDef:accessibility}.
    // @param role (String) ARIA role for this item
    // @group accessibility
    // @visibility internal
    //<
    setAriaRole : function (role) {
        var focusElement = this.getFocusElement();
        if (focusElement != null) isc.Canvas.setAriaRole(focusElement, role);
    },

    setOuterAriaRole : function (outerRole) {
        var outerElement = this.getHandle();
        if (outerElement != null) isc.Canvas.setAriaRole(outerElement, outerRole);
    },

    //> @method formItem.setAriaState()
    // Sets some ARIA state value for this FormItem.
    // Usually does not need to be manually set - see
    // +link{groupDef:accessibility}.
    // @param stateName (String) ARIA state name to set
    // @param stateValue (String | Boolean | Integer) value for the specified state
    // @group accessibility
    // @visibility internal
    //<
    setAriaState : function (stateName, stateValue) {
        var element;
        if (this.outerAriaRole) {
            element = this.getHandle();
        } else {
            element = this.getFocusElement();
        }
        if (element != null) isc.Canvas.setAriaState(element, stateName, stateValue);
    },
    setAriaStates : function (state) {
        var element;
        if (this.outerAriaRole) {
            element = this.getHandle();
        } else {
            element = this.getFocusElement();
        }
        if (element != null) isc.Canvas.setAriaStates(element, state);
    },
    clearAriaState : function (stateName) {
        var element;
        if (this.outerAriaRole) {
            element = this.getHandle();
        } else {
            element = this.getFocusElement();
        }
        if (element != null) isc.Canvas.clearAriaState(element, stateName);
    },
    getAriaState : function () {
        var state = {};

        // http://www.w3.org/WAI/PF/aria/states_and_properties#aria-required
        if (this.required && this.form && this.form.hiliteRequiredFields) state.required = true;

        // http://www.w3.org/WAI/PF/aria/states_and_properties#aria-invalid
        if (this.hasErrors()) {
            state.invalid = true;

            // When showInlineErrors is false, a list of the errors is written out at the top
            // of the form - see DynamicForm.getErrorsHTML().
            //
            // If writing out inline errors, prefer to describe-by the error text rather than an error
            // icon because some screen readers do not handle reading the error icon's 'aria-label'.

            if (!this.form.showInlineErrors || this.shouldShowErrorText()) {
                var errorMessageID = this._getErrorMessageID();

                state.describedby = errorMessageID;

            } else if (this.form.showInlineErrors && this.shouldShowErrorIcon()) {
                var errorIconID = this.getErrorIconId();

                state.describedby = errorIconID;
            }

        }

        // If there is no describedby state yet, set it to the ID of the hint cell.
        if (state.describedby == null && !this._getShowHintInField()) {
            var hintHTML = this.getHint();
            if (hintHTML) {
                var hintCellID = this._getHintCellID();

                state.describedby = hintCellID;
            }
        }

        // Disabled also means it's not in the tab order so won't be read by default.  However the spec
        // below mentions this is the case so presumably this is for screen readers to add features to
        // allow users to have disabled fields read.
        // http://www.w3.org/WAI/PF/aria/states_and_properties#aria-disabled
        if (this.isDisabled()) state.disabled = true;

        if (isc.isA.CheckboxItem(this)) state.checked = !!this.getValue();

        return state;
    },

    // called after a FormItem is drawn
    addContentRoles : function () {
        if (!isc.Canvas.ariaEnabled() || isc.Canvas.useLiteAria()) return;

        if (!this._canFocus() || !this.ariaRole) return;

        this.setAriaRole(this.ariaRole);

        var outerElement;
        if (this.outerAriaRole) {
            outerElement = this.getHandle();
            if (outerElement != null) isc.Canvas.setAriaRole(outerElement, this.outerAriaRole);
        }

        // with a visible title, we write out <label for=>, but we need an explicit aria-label if the
        // title is either not visible or if we do not have a native HTML input element (since
        // <label for=> is intended for true HTML input elements).


        if (this.title) {
            var labelElementID = this._getLabelElementID(),
                labelElement = this.getDocument().getElementById(labelElementID);

            var titleElement;
            if (this.hasDataElement()) {
                titleElement = this.getDataElement();
            } else if (this.outerAriaRole) {
                titleElement = outerElement != null ? outerElement : this.getHandle();
            } else if (isc.isA.ContainerItem(this)) {
                titleElement = this._getTableElement();
            } else {
                titleElement = this._getTextBoxElement();
            }
            if (titleElement != null) {
                // According to WAI-ARIA:
                // "If the label text is visible on screen, authors SHOULD use 'aria-labelledby'
                // and SHOULD NOT use 'aria-label'."
                // http://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby
                //
                // Thus, if we have a <label> element, use 'aria-labelledby'.
                if (labelElement != null) {
                    isc.Canvas.setAriaState(titleElement, "labelledby", labelElementID);
                } else {
                    var title = this.getTitle();
                    // Because the title is an HTMLString value, we need to remove HTML.
                    if (title != null) {
                        title = String(title).unescapeHTML();
                    }
                    isc.Canvas.setAriaState(titleElement, "label", title);
                    //this.logWarn("applied aria-label to: " + this.echo(titleElement));
                }
            }


        }

        // instance default state such as multiline:true for TextArea
        if (this.ariaState) this.setAriaStates(this.ariaState);

        // dynamic state
        this.setAriaStates(this.getAriaState());

        // If we are modeling a SelectItem as a listbox with one option, then set 'aria-selected'
        // on the text box element, which contains the SelectItem's value.
        if (isc.isA.SelectItem(this) && this.outerAriaRole === "listbox") {
            var textBox = this._getTextBoxElement();
            if (textBox != null) {
                isc.Canvas.setAriaStates(textBox, {
                    selected: true
                });
            }
        }
    }
});

isc.TextAreaItem.addProperties({
    ariaState : { multiline : true }
});

isc.RadioItem.addProperties({
    ariaRole: "radio"
});


isc.ComboBoxItem.addProperties({
    ariaState: {

        autocomplete: "list",

        // "The combobox must have aria-expanded = true if the list is displayed or aria-expanded = false
        // when it is not."
        // http://www.w3.org/WAI/PF/aria-practices/#combobox
        expanded: false
    },
    ariaRole: "combobox"
    //pickListAriaRole:"list", // not implemented
    //pickListItemAriaRole:"listitem" // not implemented
});


isc.SelectItem.addProperties({
    ariaRole:"option",
    outerAriaRole:"listbox",
    ariaState: {
        expanded: false,

        // To indicate that this single-option listbox has a popup listbox, we need to apply
        // aria-haspopup = true.
        // http://www.w3.org/TR/wai-aria-practices/#relations_haspopup
        haspopup: true
    },

    // This prevents VoiceOver from reading "space" for the value of an empty SelectItem.
    emptyDisplayValue: "<span aria-hidden='true'>&nbsp;</span>"
});

isc.StaticTextItem.addProperties({
    ariaRole:"textbox",

    ariaState:{ disabled:true }
});

// "menu" role vs "list" role: somewhat ambiguous, as both roles have the notions of
// selectability (via "checked" for menus), but generally menus show actions and sometimes
// choices whereas lists show just choices.
// ListGrid currently advertises itself as a List, it's subclass ScrollingMenu sounds like it
// should advertise itself as a Menu, however it is not used for anything but the PickList
// NOTE: separators already handled by ListGrid superclass
//isc.PickListMenu.addProperties({
//    ariaRole:"list",
//    rowRole:"listitem"
//});

isc.PickListMenu.addProperties({

    // For role="combobox", the associated listbox must have role="listbox" because a listbox
    // is a required owned element of a combobox: http://www.w3.org/TR/wai-aria/roles#combobox
    // For role="listbox", the associated options must have role="option"
    // http://www.w3.org/TR/wai-aria/roles#listbox
    ariaRole: "listbox",
    rowRole: "option",

    _$rowIdTemplate: [null, "_row_", null,null,null,null,null],
    getRowElementId : function (rowNum) {
        var template = this._$rowIdTemplate;
        template[0] = this.getID();
        isc._fillNumber(template, rowNum, 2, 5);
        return template.join(isc.emptyString);
    }

});

isc.ContainerItem.addProperties({
    setAriaRole : function (role) {
        var tableElement = this._getTableElement();
        if (tableElement != null) isc.Canvas.setAriaRole(tableElement, role);
    },
    setAriaState : function (stateName, stateValue) {
        var tableElement = this._getTableElement();
        if (tableElement != null) isc.Canvas.setAriaState(tableElement, stateName, stateValue);
    },
    setAriaStates : function (state) {
        var tableElement = this._getTableElement();
        if (tableElement != null) isc.Canvas.setAriaStates(tableElement, state);
    },
    clearAriaState : function (stateName) {
        var tableElement = this._getTableElement();
        if (tableElement != null) isc.Canvas.clearAriaState(tableElement, stateName);
    }
});

isc.RadioGroupItem.addProperties({
    ariaRole: "radiogroup"
});

} // end if (isc.DynamicForm)

if (isc.GridRenderer) {

// Grids
// ---------------------------------------------------------------------------------------


// Support for row and cell roles and states
isc.GridRenderer.addMethods({
    setRowAriaState : function (rowNum, stateName, stateValue) {
        var row = this.getTableElement(rowNum);
        if (row == null) return;
        isc.Canvas.setAriaState(row, stateName, stateValue);
    },
    setRowAriaStates : function (rowNum, state) {
        var row = this.getTableElement(rowNum);
        if (row == null) return;
        isc.Canvas.setAriaStates(row, state);
    }





});

isc.ListGrid.addMethods({
    ariaRole:"list",
    rowRole:"listitem",
    getRowRole : function (rowNum, record) {
        if (record && record.isSeparator) return "separator";
        return this.rowRole;
    },
    getRowAriaState : function (rowNum, record) {
        if (!isc.Canvas.ariaEnabled() || isc.Canvas.useLiteAria()) return; // too expensive to enable by default

        // if only rendering a range of rows, need to tell the reader the total size and position
        var state;
        if (!this.showAllRecords && this.data != null) {
            state = { setsize : this.getTotalRows(),
                      posinset : rowNum + 1 }
        }

        var value = record != null ? record : rowNum;
        if (this.selection && this.selection.isSelected && this.selection.isSelected(value)) {
            if (state == null) state = {}
            state.selected = true;
        }
        return state;
    }
});

isc.TreeGrid.addMethods({
    ariaRole:"tree",
    rowRole:"treeitem",
    getRowRole : function (rowNum, node) {
        return this.rowRole;
    },
    // an attempt to use the hasparent attribute to link nodes.  Not respected by FF1.5
    //getRowElementId : function (rowNum) {
    //    return this.getID() + "_row_" + rowNum;
    //},
    getRowAriaState : function (rowNum, node) {
        if (!isc.Canvas.ariaEnabled() || isc.Canvas.useLiteAria()) return; // too expensive to enable by default

        var theTree = this.data,
            selected = !!(this.selection && this.selection.isSelected &&
                            this.selection.isSelected(node, true)),
            level = theTree.getLevel(node);

        var state = { selected : selected,
                      level : level,
                      // if only rendering a range of rows, need to tell the reader the
                      // total size and position
                      setsize : this.getTotalRows(),
                      posinset : rowNum + 1
                    };

        if (theTree.isFolder(node)) state.expanded = !!theTree.isOpen(node);

        // an attempt to use the hasparent attribute to link nodes.  Not respected by FF1.5
        //var parent = theTree.getParent(node);
        //if (parent && parent != theTree.getRoot()) {
        //    state.hasparent = this.getRowElementId(theTree.indexOf(parent));
        //}

        return state;
    }
});

// NOTE: CubeGrid support in AnalyticsScreenReader.js

// Menus / ListPickers
// ---------------------------------------------------------------------------------------

isc.Menu.addMethods({
    ariaRole:"menu",
    // get rid of the "/" cell separators since we commonly
    // have empty cols and we don't want to render out seemingly random slashes
    screenReaderCellSeparator:null,
    getRowRole : function (rowNum, item) {
        if (!item || item.isSeparator) return "separator";
        if (item.checked || item.checkIf || item.checkable) return "menuitemcheckable";
        if (item.radio) return "menuitemradio";
        return "menuitem";
    },
    getRowAriaState : function (rowNum) {
        if (this.hasSubmenu(this.getItem(rowNum))) return { haspopup:true };
    }
});

// There is no "menubutton" role, but with aria-haspopup NVDA 2011.1.1 at least reads
// this as "menubutton submenu".
isc.MenuButton.addProperties({
    ariaRole:"button",
    ariaState:{ haspopup:true }
});
isc.MenuBar.addProperties({
    ariaRole:"menubar"
});

} // end if (isc.GridRenderer)


if (isc.RichTextEditor) {

isc.ListPropertiesSampleTile.addMethods({
    ariaState: {},
    _ariaLabelMap: {
        "disc": "Bullets",
        "circle": "Circles",
        "square": "Filled squares",
        "decimal": "Numbers",
        "upper-roman": "Uppercase Roman numerals",
        "lower-roman": "Lowercase Roman numerals",
        "upper-alpha": "Uppercase letters",
        "lower-alpha": "Lowercase letters"
    },
    _otherUnorderedListAriaLabel: "Other bulleted style",
    _otherOrderedListAriaLabel: "Other numbered style",
    getAriaState : function () {
        var state = isc.addProperties({}, this.ariaState);

        var listProperties = this._canonicalProperties,
            style = listProperties.style;
        if (style == "custom-image") {
            var image = listProperties.image;
            var lastSlashPos = image.lastIndexOf('/');
            if (lastSlashPos >= 0) {
                image = image.substring(lastSlashPos + 1);
            }
            state.label = "Custom bullet image '" + image + "'";
        } else if (this._ariaLabelMap.hasOwnProperty(style)) {
            state.label = this._ariaLabelMap[style];
        } else {
            var isUnordered = isc.ListPropertiesPane.getListType(listProperties) == "unordered";
            state.label = isUnordered ? this._otherUnorderedListAriaLabel
                                      : this._otherOrderedListAriaLabel;
        }

        return state;
    },
    getAriaStateAttributes : function () {
        return isc.Canvas.getAriaStateAttributes(this.getAriaState());
    }
});

isc.ListPropertiesPane.addProperties({
    //< @attr listPropertiesPane.sampleTileLayoutAriaLabel (String : "List style" : IR)
    // The ARIA label to use for the +link{ListPropertiesPane.sampleTileLayout,sampleTileLayout}.
    // @group i18nMessages
    //<
    sampleTileLayoutAriaLabel: "List style"
});

isc.ListPropertiesPane.changeDefaults("sampleTileLayoutDefaults", {
    ariaRole: "radiogroup",
    ariaState: {
        haspopup: false
    },
    init : function () {
        this.Super("init", arguments);
        this.ariaState = isc.addProperties({}, this.ariaState, {
            label: this.creator.sampleTileLayoutAriaLabel
        });
    }
});

isc.ListPropertiesPane.changeDefaults("sampleTileDefaults", {
    ariaRole: "radio",
    setSelected : function () {
        this.Super("setSelected", arguments);
        this.setAriaState("checked", this.isSelected());
    }
});

} // end if (isc.RichTextEditor)


if (isc.Window) {
isc.Window.addProperties({
    ariaState: {
        labelledby: null,
        describedby: null
    },
    getAriaState : function () {

        var state = isc.addProperties({}, this.ariaState);

        var headerLabel = this.headerLabel;
        if (headerLabel != null && headerLabel.isVisible()) state.labelledby = headerLabel._getAriaHandleID();
        var body = this.body;
        if (body != null && body.isVisible()) state.describedby = body._getAriaHandleID();
        return state;
    },
    getAriaStateAttributes : function () {
        return isc.Canvas.getAriaStateAttributes(this.getAriaState());
    }
});
}

if (isc.Dialog) {
isc.Dialog.addProperties({

    getAriaState : function () {
        var state = isc.addProperties({}, this.Super("getAriaState", arguments));
        var messageLabel = this.messageLabel;
        if (messageLabel != null && messageLabel.isVisible()) state.describedby = messageLabel._getAriaHandleID();
        return state;
    }
});
}


(function () {
    var roleMap = {
        Button : "button",
        StretchImgButton : "button",
        ImgButton : "button",
        Label : "label",

        // Section stacks - headers are "heading"s

        SectionHeader:"heading",
        ImgSectionHeader:"heading",

        // FormItems
        CheckboxItem : "checkbox",
        Slider : "slider",

        TextItem : "textbox",

        // TextArea is textbox + plus multiple:true state
        // http://www.w3.org/WAI/PF/aria/states_and_properties#aria-multiline
        TextAreaItem : "textbox",

        Window : "dialog",
        Dialog : "alertdialog",
        Toolbar : "toolbar",

        // a good default.  Without this NVDA will read an HTMLFlow as just "section" and stop.  With
        // this, contents are read.
        HTMLFlow:"article",
        HTMLPane:"article",

        // not doing this by default since lots of components use Layouts in various internal
        // ways that do not correspond to a "group"
        //Layout : "group",

        // NOTE example shows 'tablist' element surrounding 'tab's but not 'tabpanel's
        // http://www.mozilla.org/access/dhtml/class/tabpanel
        TabBar : "tablist",
        PaneContainer : "tabpanel",
        ImgTab : "tab",

        EdgedCanvas : "presentation",
        BackMask : "presentation"

    }
    for (var className in roleMap) {
        var theClass = isc.ClassFactory.getClass(className);
        if (theClass) theClass.addProperties({ariaRole:roleMap[className]});
    }
})();




// ----------------------------------------------------------------------------------------

// If ListGrid, or DynamicForm isn't loaded don't attempt to create this class - it's a requirement.
if (isc.ListGrid != null && isc.DynamicForm != null) {

// Utility class for picking foreignKey and includeFrom properties in the DataSourceEditor.
// Provide validDsNames on creation (or set them when showing the picker).
// Fires "changed(form, value)" when the combinedValue changes, returning it as dsName.fieldName
isc.ClassFactory.defineClass("DataSourceFieldPicker", "DynamicForm");

isc.DataSourceFieldPicker.addClassProperties({
    // The warning text where we can't guess foreign keys.
    FOREIGN_KEY_WARNING: "Could not guess which foreignKey to use. " +
                          "Determine which of your fields is the foreign key, " +
                          "and make its foreignKey property point to a field in ",

    getForeignKeyWarning : function(foreignDsName) {
        return isc.DataSourceFieldPicker.FOREIGN_KEY_WARNING + "'" + foreignDsName + "'.";
    }
});

isc.DataSourceFieldPicker.addProperties({
    // An array of valid DataSource names. The DataSources need not be loaded -- the picker
    // will lazily call isc.DS.load when necessary to get field names.
    // validDsNames: null,

    // An array of records representing all known DataSources, even if we don't know that they
    // are valid yet. If chosen, they can be lazily validated via a callback.
    // If both validDsNames and allDSNames are provided, then we will offer both lists in the
    // drop down menu, with a separator in between.
    // allDsRecords: null,

    // The required base type of the field. Leave null if any field type is fine.
    // requiredBaseType: null,

    // Try to guess a foreign key with this datasource record. The record should contain
    // at least an ID and an array of field records
    // warnIfNoForeignKey: null,

    // The value of the dsName and fieldName, in the form dsName.fieldName
    // combinedValue: "",

    fields: [{
        name: "DataSource",
        type: "Select",

        // Restore default titleStyle, because the tool skin doesn't seem to
        // be applied consistently in the picker
        titleStyle: "formTitle",

        // Allow empty values so that the user can choose no DataSourceField
        allowEmptyValue: true,

        valueField: "ID",
        getClientPickListData : function() {
            return this.form.getDatasourcePickListData();
        },

        changed : function(form, item, value) {
            form.handleDsNameChanged(value);
        }
    },{
        name: "Field",
        type: "Select",

        // Restore default titleStyle, because the tool skin doesn't seem to
        // be applied consistently in the picker
        titleStyle: "formTitle",

        changed : function(form, item, value) {
            form.handleChanged();
        }
    }],

    getDatasourcePickListData : function () {
        // Setting validDsNames or allDsRecords will reset _datasourcePickListData, causing a
        // lazy recalculation here.
        if (!this._datasourcePickListData) {
            // Always allow an empty value!
            this._datasourcePickListData = [""];

            if (this.validDsNames && this.validDsNames.getLength() > 0) {
                this._datasourcePickListData.addList(this.validDsNames.map(function (dsName) {
                    return {ID: dsName};
                }));
            }

            if (this.allDsRecords && this.allDsRecords.getLength() > 0) {
                if (this._datasourcePickListData.getLength() > 0) {
                    this._datasourcePickListData.add({isSeparator: true});
                }

                // allDsRecords is already an array of records ...
                this._datasourcePickListData.addList(this.allDsRecords);
            }
        }

        return this._datasourcePickListData;
    },

    setValidDsNames : function(validDsNames) {
        this.validDsNames = validDsNames;
        this._datasourcePickListData = null;
    },

    setAllDsRecords : function(allDsRecords) {
        this.allDsRecords = allDsRecords;
        this._datasourcePickListData = null;
    },

    setWarnIfNoForeignKey : function(dsRecord) {
        this.warnIfNoForeignKey = dsRecord;
    },

    setCombinedValue : function(value) {
        var dsItem = this.getItem("DataSource");
        var fieldItem = this.getItem("Field");

        var parts = (value || "").split(".");
        dsItem.setValue(parts[0]);
        fieldItem.setValue(parts[1]);

        this.handleDsNameChanged(parts[0]);
    },

    getCombinedValue : function() {
        var value = this.getValue("DataSource");
        var fieldName = this.getValue("Field");
        if (fieldName) value = value + "." + fieldName;
        return value;
    },

    initWidget : function() {
        this.Super("initWidget", arguments);

        if (this.combinedValue) this.setCombinedValue(this.combinedValue);
    },

    _warnIfCannotGuessForeignKey : function (foreignDS) {
        var ourDsRec = this.warnIfNoForeignKey;

        if (!ourDsRec || !ourDsRec.fields) return;

        // If we have a field with a foreignKey defined that points to the foreignDS,
        // then we're fine.
        var foreignKeys = ourDsRec.fields.map(function (field) {
            return field.foreignKey ? field.foreignKey.split('.')[0] : null;
        });
        if (foreignKeys.contains(foreignDS.ID)) return;

        // We would also be fine if the foreignDS has a field with a foreignKey that
        // points back to us.
        foreignKeys = foreignDS.getFieldNames().map(function (fieldName) {
            var field = foreignDS.getField(fieldName);
            return field.foreignKey ? field.foreignKey.split('.')[0] : null;
        });
        if (foreignKeys.contains(ourDsRec.ID)) return;

        // We are also fine if there is a field in ourDsRec and foreignDsRec with
        // matching names
        var ourFieldNames = ourDsRec.fields.getProperty("name");
        var foreignFieldNames = foreignDS.getFieldNames();
        if (ourFieldNames.intersect(foreignFieldNames).getLength() > 0) return;

        // If we've gotten this far, then we can't guess the foreignKey. So, we'll
        // display a warning. Note that while we're using the standard error mechanism
        // for DyanmicForms, we don't actually prevent the update from occurring -- the
        // user can save this value if they like -- it's just a warning.
        this.addFieldErrors("DataSource",
            isc.DataSourceFieldPicker.getForeignKeyWarning(this.getValue("DataSource")),
            true);
    },

    // Callback when we load the live DS that corresponds to the dsName chosen
    // Can be called with null if loading the DS from the server has failed
    handleLiveDs : function(ds) {
        var fieldNames = [];
        var field = this.getField("Field");

        if (ds) {
            // Figure out the available field names
            fieldNames = ds.getFieldNames();
            if (this.requiredBaseType) {
                var self = this;
                fieldNames = fieldNames.findAll(function(field) {
                    var baseType = isc.SimpleType.getBaseType(ds.getField(field).type, ds);
                    return baseType == self.requiredBaseType;
                });
            }

            // If there is only one possible value, we may as well autodetect it
            if (fieldNames.getLength() == 1) {
                field.setValue(fieldNames[0]);
                this.handleChanged();
            }

            // If our current value isn't possible, then reset it
            if (!fieldNames.contains(field.getValue())) {
                field.setValue("");
                this.handleChanged();
            }

            // If we're checking for possible foreignKeys, then validate them
            if (this.warnIfNoForeignKey) this._warnIfCannotGuessForeignKey(ds);
        }

        field.setValueMap(fieldNames);
    },

    handleDsNameChanged : function(dsName) {
        // If value is empty, then may as well reset the field too
        if (!dsName) this.getField("Field").setValue("");

        // And fire the change event
        this.handleChanged();

        // Reset the value map for the fields. This will get filled in to
        // the available values when we get the live DS.
        this.getField("Field").setValueMap([]);

        // And reset any warnings about foreign keys -- that will also get
        // checked when we get the live DS.
        this.clearFieldErrors("DataSource", true);

        // Try to get the live DS that corresponds to the dsName chosen,
        // either synchronously or asynchronously
        if (dsName) {
            var ds = isc.DS.get(dsName);
            if (ds) {
                this.handleLiveDs(ds);
            } else {
                var self = this;
                isc.DS.load(dsName, function() {
                    ds = isc.DS.get(dsName);
                    if (!ds) self.logWarn("Loading dataSource from server was unsuccessful for " + dsName);
                    self.handleLiveDs(ds);
                });
            }
        }
    },

    handleChanged : function() {
        if (this.changed) this.changed(this, this.getCombinedValue());
    }
});

isc.DataSourceFieldPicker.registerStringMethods({
    // Fired when the combinedValue in the picker changes
    changed : "form, value"
});

//> @class DataSourceEditor
// Provides a UI for creating and editing +link{DataSource, DataSources).
//
// @visibility devTools
//<
isc.ClassFactory.defineClass("DataSourceEditor", "VLayout");

isc.DataSourceEditor.addProperties({
// attributes
overflow: "visible",


//> @attr dataSourceEditor.dataSource (DataSource or ID : null : IRW)
// DataSource being edited.
//
// @visibility devTools
//<

//> @attr dataSourceEditor.knownDataSources (Array : null : IRW)
// A list of all known DataSources, to be used when editing foreign keys.
// Each element of the array should be a record with at least ID and type properties.
//
// @visibility devTools
//<

//> @attr dataSourceEditor.mainEditor (AutoChild ComponentEditor : null : IRW)
//
// @visibility devTools
//<
mainEditorDefaults: {
    _constructor: "ComponentEditor",
    autoDraw:false,
    numCols:8,
    overflow:"visible",
//    backgroundColor: "black",
    dataSource:"DataSource",
    fields : [
        {name:"ID", title: "ID", required:true},
        {name:"dropExtraFields"},
        {name:"autoDeriveSchema"},
        //{name:"dataFormat", defaultValue:"iscServer", redrawOnChange:true},

        {type:"section", defaultValue:"XPath Binding", showIf:"values.dataFormat != 'iscServer'",
         itemIds:["dataURL", "selectBy", "recordXPath", "recordName"]},
        {name:"dataURL", showIf:"values.dataFormat != 'iscServer'"},
        {name:"selectBy", title:"Select Records By",
         shouldSaveValue:false,
         valueMap:{ tagName:"Tag Name", xpath:"XPath Expression" },
         defaultValue:"xpath",
         redrawOnChange:true,
         // can't use tagName in JSON
         showIf:"values.dataFormat == 'xml'"},
        // allowed in XML or JSON
        {name:"recordXPath",
         showIf:"values.dataFormat != 'iscServer' && form.getItem('selectBy').getValue() == 'xpath'"},
        // allow in XML only
        {name:"recordName",
         showIf:"values.dataFormat == 'xml' && values.selectBy == 'tagName'"},

        {type:"section", defaultValue:"SQL Binding",
         showIf:"values.serverType == 'sql' || values.serverType == 'hibernate'",
         itemIds:["dbName", "schemaName", "tableName"]},
        {name:"dbName", showIf:"values.serverType == 'sql'"},
        {name:"schema", showIf:"values.serverType == 'sql'"},
        {name:"tableName",
         showIf:"values.serverType == 'sql' || values.serverType == 'hibernate'"},
        {name:"beanClassName",
         showIf:"values.serverType == 'sql' || values.serverType == 'hibernate'"},

        {type:"section", defaultValue:"Record Titles", sectionExpanded:false,
         itemIds:["title", "pluralTitle", "titleField"]},
        {name:"title"},
        {name:"pluralTitle"},
        {name:"titleField"}
    ]
},

fieldEditorDefaults: {
    _constructor: "ListEditor",
    autoDraw:false,
    inlineEdit:true,
    dataSource:"DataSourceField",
    saveLocally:true,
    gridButtonsOrientation:"right",
    fields:[
        {name:"name", treeField: true,
            // Where includeFrom has been used, the name defaults to includeFrom's name.
            // So as well show that instead of nothing. We'll put it in italics to indicate
            // that it is special.
            //
            // In fact, we may as well show the includeFrom value in all cases (where
            // present) -- this will help avoid confusion where the name has been edited.
            formatCellValue : function(value, record, rowNum, colNum, grid) {
                var formattedValue = this._nameFromValueOrIncludeFrom(value, record.includeFrom);
                if (record.includeFrom) {
                    formattedValue +=" <i>[" + record.includeFrom + "]</i>";
                }
                return formattedValue;
            },

            // If the value is present, return it. Otherwise, return the last
            // part of the includeFrom -- which is what the name defaults to.
            _nameFromValueOrIncludeFrom : function(value, includeFrom) {
                if (value) {
                    return value;
                } else {
                    var dotIndex = includeFrom.lastIndexOf(".");
                    if (dotIndex == -1) {
                        return value;
                    } else {
                        return includeFrom.substring(dotIndex + 1);
                    }
                }
            },

            // Note that name is *required* in the schema. This isn't literally true
            // anymore, since now name is optional when includesFrom is specified.
            // We could make it optional in the schema, but that may cause difficulties
            // elsewhere. So, for the moment, we're doing some massaging here.
            //
            // For editing, we'll display the last part of the includeFrom if the name
            // is blank -- that is what the default actually is, so it is a reasonable
            // starting point for editing.
            formatEditorValue : function(value, record, form, item) {
                return this._nameFromValueOrIncludeFrom(value, record.includeFrom);
            },

            // If the user blanks the value, it would normally be an error (since
            // the name is required. So, let's simply supply the default in that
            // case -- that is, use the includeFrom's name. The alternative would
            // be to allow the blank, but that would mean changing the schema so that
            // name would not be required.
            parseEditorValue : function(value, record, rowNum, colNum, grid) {
                return this._nameFromValueOrIncludeFrom(value, record.includeFrom);
            }
        },
        {name:"title"},
        {name:"type", width:60},
        {name:"required", title:"Req.", width:40, canToggle:true},
        {name:"hidden", width:40},
        {name:"length", width:60},
        {name:"primaryKey", title:"is PK", width:40}
    ],
    formProperties: {
        numCols:4,
        initialGroups:10
    },
    formFields : [
        {name:"name", canEdit:false},
        {name:"type"},
        {name:"title"},
        {name:"primaryKey"},
        {name:"valueXPath", colSpan:2,
            showIf:function () {
                var grid = this.form.creator,
                    mainEditor = grid ? grid.creator.mainEditor : null;
                return (mainEditor && mainEditor.getValues().dataFormat != 'iscServer');

            }
        },

        {type:"section", defaultValue:"Value Constraints",
         itemIds:["required", "length", "valueMap"] },
        {name:"valueMap", rowSpan:2},
        {name:"required"},
        {name:"length"},

        {type:"section", defaultValue:"Component Binding",
         itemIds:["hidden", "detail", "canEdit"] },
        {name:"canEdit"},
        {name:"hidden"},
        {name:"detail"},

        {type:"section", defaultValue:"Relations", sectionExpanded:true,
         itemIds:["foreignKey", "rootValue", "includeFrom"] },
        {
            name: "foreignKey",
            type: "staticText",

            showPickerIcon: true,
            pickerConstructor: "DataSourceFieldPicker",
            pickerProperties: {
                width : 160,
                changed : function(form, value) {
                    form.creator.setValue(value);
                }
            },

            // Override showPicker to set up the valid DataSources based on whatever
            // edits we've done.
            showPicker : function() {
                // Look up the creator chain for the DataSourceEditor
                var dsEditor = this;
                while (dsEditor && !isc.isA.DataSourceEditor(dsEditor)) dsEditor = dsEditor.creator;
                if (!dsEditor) {
                    this.logWarn("Could not find the DataSourceEditor");
                    return;
                }
                if (!dsEditor.knownDataSources) {
                    this.logWarn("DataSourceEditor.knownDataSources has not been set");
                    return;
                }

                // Actually show the picker and set the valid Datasources
                this.Super("showPicker", arguments);

                var dsData = dsEditor.getDatasourceData();

                var validDS = dsEditor.knownDataSources.findAll({dsType: dsData.serverType});
                this.picker.setValidDsNames(validDS.getProperty("ID"));

                // Try to get the live DS we are editing, in case there are types defined on the DS
                var ds = isc.DS.get(dsData.ID);
                this.picker.requiredBaseType = isc.SimpleType.getBaseType(this.form.getValue("type"), ds);

                this.picker.setCombinedValue(this.getValue());
            },

            // Pickers aren't destroyed by default, so we'll do that here
            destroy : function() {
                if (this.picker) this.picker.destroy();
                this.Super("destroy", arguments);
            }
        },
        {name:"rootValue"},
        {
            name: "includeFrom",
            type: "staticText",

            showPickerIcon: true,
            pickerConstructor: "DataSourceFieldPicker",
            pickerProperties: {
                changed : function(form, value) {
                    form.creator.setValue(value);
                }
            },

            // Override showPicker to set up the valid DataSources based on whatever
            // edits we've done.
            showPicker : function() {
                // Look up the creator chain for the DataSourceEditor
                var dsEditor = this;
                while (dsEditor && !isc.isA.DataSourceEditor(dsEditor)) dsEditor = dsEditor.creator;
                if (!dsEditor) {
                    this.logWarn("Could not find the DataSourceEditor");
                    return;
                }

                var dsData = dsEditor.getDatasourceData();

                // The known valid datasources are the ones for which we have a foreignKey
                // defined, since includeFrom only works one level at a time. So, as a first
                // approximation, we can just collect the foreignKey's we've defined for this
                // dataSource. Note that we get them from the form, rather than the real
                // dataSource, so that we can immediately react to any changes.
                var editedFieldData = dsData.fields;

                var fieldsWithForeignKeys = editedFieldData.findAll(function (field) {
                    return field.foreignKey;
                }) || [];
                var validDsNames = fieldsWithForeignKeys.map(function (field) {
                    return field.foreignKey.split(".")[0];
                }).getUniqueItems();

                // It is also possible that foreignKeys can be guessed for other datasources,
                // but we won't know until they are chosen and lazily loaded. So, we also
                // supply a list of datasources of the same type as ours
                var allDsRecords = null;
                if (dsEditor.knownDataSources) {
                    var ourType = dsData.serverType;
                    if (ourType) {
                        allDsRecords = dsEditor.knownDataSources.findAll({dsType: ourType});
                    } else {
                        allDsRecords = dsEditor.knownDataSources;
                    }
                }

                // Actually show the picker (possibly creating it)
                this.Super("showPicker", arguments);

                this.picker.setValidDsNames(validDsNames);
                if (allDsRecords) this.picker.setAllDsRecords(allDsRecords);

                // In order to allow validation of lazily loaded datasources, we provide
                // our ID and field records
                this.picker.setWarnIfNoForeignKey(dsData);

                this.picker.setCombinedValue(this.getValue());
            },

            // Pickers aren't destroyed by default, so we'll do that here
            destroy : function() {
                if (this.picker) this.picker.destroy();
                this.Super("destroy", arguments);
            }
        }
    ],
    gridDefaults:{
        editEvent:"click",

        listEndEditAction:"next",
        autoParent:"gridLayout",
        selectionType:isc.Selection.SINGLE,
        recordClick:"this.creator.recordClick(record)",
        modalEditing:true,
        editorEnter:"if (this.creator.moreButton) this.creator.moreButton.enable()",
        selectionChanged: function() {
            if (this.anySelected() && this.creator.moreButton) {
                this.creator.moreButton.enable();
            }
        },
        contextMenu : {
            data : [
                {title:"Remove", click: "target.creator.removeRecord()" }
            ]
        },
        // get rid of default LG borders
        styleName:"rightBorderOnly",
        validateByCell:true,
        leaveScrollbarGap:false,
        alternateRecordStyles:true,
        // show a delete column
        canRemoveRecords:true,
        canEdit: true,
        canEditCell : function (rowNum, colNum) {
            var record = this.getRecord(rowNum),
                field = this.getField(colNum),
                fieldName = field[this.fieldIdProperty],
                isNameOrTitle = (fieldName == "name" || fieldName == "title");
            if (isc.isA.TreeGrid(this)) {
                if (record.isFolder &&
                !(isNameOrTitle || fieldName == "required" || fieldName == "hidden")) {
                    return false;
                }
            }
            else {
                if (this.getDataSource().fieldIsComplexType(field) && !isNameOrTitle)
                    return false;
            }
            return this.Super('canEditCell', arguments);
        }

    },

    newRecord : function () {
        if (this.creator.canEditChildSchema) {
            var grid = this.grid,
                tree = grid.data,
                selectedNode = this.getSelectedNode();

            if (!selectedNode) selectedNode = tree.root;
            var parentNode = tree.getParent(selectedNode)

            if (selectedNode) {
                if (!selectedNode.isFolder) selectedNode = parentNode;
                var newNode = {
                    name: this.getNextUniqueFieldName(selectedNode, "field"),
                    id: this.getNextUnusedNodeId(),
                    parentId: selectedNode ? selectedNode.id : null
                };
                this.addNode(newNode, selectedNode);
            }
        } else this.Super("newRecord", arguments);
    },
    getSelectedNode : function () {
        return this.grid.getSelectedRecord();
    },
    addNode : function (newNode, parentNode) {
        var tree = this.grid.data;

        tree.linkNodes([newNode]);
    },
    getNextUniqueFieldName : function (node, prefix) {
        var childFields = node ? node.fields || [] : [],
            inc=1;

        if (!prefix || prefix.length == 0) prefix = "field";
        if (childFields && childFields.length > 0) {
            for (var i = 0; i < childFields.length; i++) {
                var item = childFields.get(i),
                    itemName = item.name;
                if (itemName.substring(0, prefix.length) == prefix && itemName.length > prefix.length) {
                    var thisInc = parseInt(itemName.substring(prefix.length));
                    if (!isNaN(thisInc) && thisInc >= inc)
                        inc = thisInc+1;
                }
            }
        }
        return prefix + inc;
    },
    getNextUnusedNodeId : function () {
        var tree = this.grid.data;
        for (var i = 1; i<10000; i++) {
            var item = tree.findById(i);
            if (!item) return i;
        }
        return 1;
    }
},

newButtonDefaults:{
    _constructor:isc.AutoFitButton,
    autoParent:"gridButtons",
    title: "New Field",
    click:"this.creator.newRecord()"
},

moreButtonDefaults:{
    _constructor:isc.AutoFitButton,
    autoParent:"gridButtons",
    click:"this.creator.editMore()",
    disabled:true
},

buttonLayoutDefaults: {
    _constructor: "HLayout",
    width: "100%"
},

saveButtonDefaults: {
    _constructor: "IButton",
    autoDraw: false,
    title: "Save",
    autoFit: true,
    autoParent: "buttonLayout",
    click: function(){
        var valid=true;
        if (this.creator.showMainEditor != false) valid = this.creator.mainEditor.validate();
        if (valid && this.creator.fieldEditor.validate()) this.creator.save();
    }
},

addChildButtonDefaults: {
    _constructor: "IButton",
    autoDraw: false,
    title: "Add Child Object",
    autoFit: true,
    click: function() {
        var editor = this.creator.fieldEditor,
            grid = editor.grid,
            tree = grid.data,
            selectedNode = grid.getSelectedRecord() || tree.root,
            parentNode = tree.getParent(selectedNode),
            newNode = {
                isFolder: true,
                children: [],
                multiple: true,
                childTagName: "item"
            }
        ;

        if (selectedNode) {
            if (!selectedNode.isFolder) selectedNode = parentNode;
            newNode.name = editor.getNextUniqueFieldName(selectedNode, "child"),
            newNode.id = editor.getNextUnusedNodeId(),
            newNode.parentId = selectedNode.id;
            tree.linkNodes([newNode], parentNode);
            tree.openFolder(newNode);
        }

    }
},

mainStackDefaults: {
    _constructor: "SectionStack",
    overflow: "visible",
    width: "100%", height:"100%",
    visibilityMode: "multiple"
},

instructionsSectionDefaults: {
    _constructor: "SectionStackSection",
    title: "Instructions",
    expanded:true, canCollapse:true
},

instructionsDefaults: {
    _constructor: "HTMLFlow",
    autoFit:true,
    padding:10
},

mainSectionDefaults: {
    _constructor: "SectionStackSection",
    title:"DataSource Properties",
    expanded:true, canCollapse:false, showHeader: false
},

fieldSectionDefaults: {
    _constructor: "SectionStackSection",
    title:"DataSource Fields &nbsp;<span style='color:#BBBBBB'>(click to edit or press New)</span>",
    expanded:true, canCollapse: true
},

deriveFieldsSectionDefaults: {
    _constructor: "SectionStackSection",
    title:"Derive Fields From SQL",
    expanded:false, canCollapse: true
},

bodyProperties:{
    overflow:"auto",
//    backgroundColor: "black",
    layoutMargin:10
},

deriveFormDefaults: {
    _constructor: "DynamicForm"
},

previewGridDefaults: {
    _constructor: "ListGrid",
    showFilterEditor: true
},

// properties
canEditChildSchema: false,
canAddChildSchema: false,

// methods
editNew : function (dataSource, callback, instructions) {
    if (dataSource.defaults) {
        this.paletteNode = dataSource;
        this.start(dataSource.defaults, callback, true, instructions);
    } else {
        this.start(dataSource, callback, true, instructions);
    }
},

editSaved : function (dataSource, callback, instructions) {
    this.start(dataSource, callback, false, instructions);
},

start : function (dataSource, callback, isNew, instructions) {
    if (instructions) {
        this.mainStack.showSection(0);
        this.instructions.setContents(instructions);
    } else {
        this.mainStack.hideSection(0);
    }

    if (this.mainEditor) this.mainEditor.clearValues();
    if (this.fieldEditor) this.fieldEditor.setData(null);

    // to be called when editing completes
    this.saveCallback = callback;

    this.logWarn("editing " + (isNew ? "new " : "" ) +
                 "DataSource: " + this.echo(dataSource));

    if (!dataSource) {
        // no initial dataSource properties at all, start editing from scratch
        return this.show();
    }

    this.dsClass = dataSource.Class;
    if (isNew) {
        // dataSource has never been saved
        if (isc.isA.DataSource(dataSource)) {
            // serializeableFields picks up the fields data - also pick up the
            // sfName if it's defined
            var sfName = dataSource.sfName;
            // currently used only for web service / SalesForce pathways, where we
            // dynamically retrieve a DataSource generated from XML schema.
            dataSource = dataSource.getSerializeableFields();
            if (sfName) dataSource.sfName = sfName;

            this.logWarn("editing new DataSource from live DS, data: " +
                         this.echo(dataSource));
        } else {
            dataSource.ID = this.getUniqueDataSourceID();
        }
        this._startEditing(dataSource);
    } else {
        // we need the clean initialization data for this DataSource (the live data
        // contains various derived state)
        isc.DMI.callBuiltin({
            methodName: "loadSharedXML",
            callback: this.getID() + "._loadSchemaReply(data)",
            arguments: [
                "DS",
                dataSource.ID
            ]
        });
    }
},

// override point to provide a unique datasource-id
getUniqueDataSourceID : function () {
    return "newDataSource";
},

_loadSchemaReply : function (data) {
    //!OBFUSCATEOK
    // instantiate the DataSource in "captureDefaults" mode, where Class.create()
    // returns a editComponent instead
    isc.captureDefaults = true;
    var dsComponent = isc.eval(data.js);
    isc.captureDefaults = null;

    var defaults = dsComponent.defaults;
    this.logWarn("captured DS defaults: " + this.echo(defaults));

    // do some automatic defaulting otherwise done at DataSource.init()
    if (defaults.serverType == "sql") defaults.dataFormat = "iscServer";
    if (defaults.recordXPath != null && defaults.dataFormat == null) {
        defaults.dataFormat = "xml";
    }

    this._startEditing(defaults);
},
_startEditing : function (defaults) {
    if (this.mainEditor) this.mainEditor.setValues(defaults);
    else this.mainEditorValues = defaults;
    var fields = defaults.fields;

    if (!isc.isAn.Array(fields)) fields = isc.getValues(defaults.fields);

    if (this.fieldEditor) {
        if (this.canEditChildSchema) {
            this.setupIDs(fields, 1, null);

            var tree = isc.Tree.create({
                modelType: "parent",
                childrenProperty: "fields",
                titleProperty: "name",
                idField: "id",
                nameProperty: "id",
                root: { id: 0, name: "root"},
                data: fields
            });
            tree.openAll();
            this.fieldEditor.setData(tree);
        } else this.fieldEditor.setData(fields);
    }
    this.show();
},

setupIDs : function (fields, nextId, parentId) {
    var index=nextId,
        item,
        subItem
    ;

    if (!index) index = 1;
    for (var i = 0; i < fields.length; i++) {
        var item = fields.get(i);
        item.parentId = parentId;
        item.id = index++;
        if (item.fields) {
            if (!isc.isAn.Array(item.fields)) item.fields = isc.getValues(item.fields);
            index = this.setupIDs(item.fields, index, item.id);
        }
    }
    return index;
},

getDatasourceData : function () {
    // NOTE: dsClass is set when we begin editing
    var dsClass = this.dsClass || "DataSource",
        dsData = isc.addProperties({},
            this.mainEditor ? this.mainEditor.getValues() : this.mainEditorValues
        )
    ;

    if (this.canEditChildSchema) {
        var tree = this.fieldEditor.grid.data,
            fields = tree.getCleanNodeData(tree.getRoot(), true).fields;

        dsData.fields = this.getExtraCleanNodeData(fields);
    } else {
        dsData.fields = this.fieldEditor.getData();
    }

    return dsData;
},

save : function () {
    var dsData = this.getDatasourceData();

    if (dsData.serverType == "sql" || dsData.serverType == "hibernate") {
        if (!dsData.fields.getProperty("primaryKey").or()) {
            isc.warn("SQL / Hibernate DataSources must have a field marked as the primary key");
            return;
        }
    }

    this.doneEditing(dsData);

},

getExtraCleanNodeData : function (nodeList, includeChildren) {
    if (nodeList == null) return null;

    var nodes = [],
        wasSingular = false;
    if (!isc.isAn.Array(nodeList)) {
        nodeList = [nodeList];
        wasSingular = true;
    }

    for (var i = 0; i < nodeList.length; i++) {
        var treeNode = nodeList[i],
            node = {};
        // copy the properties of the tree node, dropping some further Tree/TreeGrid artifacts
        for (var propName in treeNode) {
            if (propName == "id" || propName == "parentId" || propName == "isFolder") continue;

            node[propName] = treeNode[propName];

            // Clean up the children as well (if there are any)
            if (propName == this.fieldEditor.grid.data.childrenProperty && isc.isAn.Array(node[propName])) {
                node[propName] = this.getExtraCleanNodeData(node[propName]);
            }
        }
        nodes.add(node);
    }
    if (wasSingular) return nodes[0];
    return nodes;
},

doneEditing : function (dsData) {
    // handle custom subclasses of DataSource for which there is no schema defined by
    // serializing based on the DataSource schema but adding the _constructor property to
    // get the correct class.
    // XXX problem: if you ask an instance to serialize itself, and there is no schema for
    // it's specific class, it uses the superClass schema but loses it's Constructor
    // XXX we to preserve the class, we need to end up with the "constructor" property set
    // in XML, but this has special semantics in JS
    var dsClass = this.dsClass || "DataSource",
        schema;
    if (isc.DS.isRegistered(dsClass)) {
        schema = isc.DS.get(dsClass);
    } else {
        schema = isc.DS.get("DataSource");
        dsData._constructor = dsClass;
    }

    // explicit class properties:
    // - in XML: "constructor" or xsi:type in instances, or "instanceConstructor" in schema
    // - for ClassFactory.newInstance(): _constructor

    // serialize to XML and save to server
    var xml = schema.xmlSerialize(dsData);
    this.logWarn("saving DS with XML: " + xml);

    isc.DMI.callBuiltin({
        methodName: "saveSharedXML",
        arguments: [
            "DS",
            dsData.ID,
            xml
        ]
    });

    // create a live instance
    var liveDS = isc.ClassFactory.getClass(dsClass).create(dsData);

    // fire the callback passed in when editing began
    this.fireCallback(this.saveCallback, "dataSource", [liveDS]);
    this.saveCallback = null;
},
clear : function () {
    if (this.mainEditor) this.mainEditor.clearValues();
    else this.mainEditorValues = null;
    this.fieldEditor.setData([]);
},

initWidget : function () {
    this.Super('initWidget', arguments);

    this.addAutoChildren(["mainStack", "instructions", "mainEditor",
        "buttonLayout", "saveButton"]);

    if (this.canAddChildSchema) {
        this.canEditChildSchema = true;
        this.addAutoChild("addChildButton");
    }

    this.addAutoChild("fieldEditor", {
        // NOTE: provided dynamically because there's currently a forward dependency: DataSourceEditor is
        // defined in ISC_DataBinding but ComponentEditor is defined in ISC_Tools
        formConstructor:isc.TComponentEditor || isc.ComponentEditor,
        gridConstructor: this.canEditChildSchema ? isc.TreeGrid : isc.ListGrid,
        showMoreButton: this.showMoreButton,
        newButtonTitle: "New Field",
        newButtonDefaults: this.newButtonDefaults,
        newButtonProperties: this.newButtonProperties,
        moreButtonDefaults: this.moreButtonDefaults,
        moreButtonProperties: this.moreButtonProperties
    });
    this.moreButton = this.fieldEditor.moreButton;
    this.newButton = this.fieldEditor.newButton;

    if (this.canAddChildSchema) this.fieldEditor.gridButtons.addMember(this.addChildButton);

    var stack = this.mainStack;

    stack.addSections([isc.addProperties(this.instructionsSectionDefaults,
        this.instructionsSectionProperties,
        { items:[this.instructions] }
    )]);

    stack.addSections([isc.addProperties(this.mainSectionDefaults,
        this.mainSectionProperties,
        { items:[this.mainEditor] }
    )]);
    if (this.showMainEditor==false) stack.hideSection(1);

    stack.addSections([isc.addProperties(this.fieldSectionDefaults,
        this.fieldSectionProperties,
        { items:[this.fieldEditor] }
    )]);


    var _this = this;
    this.deriveForm = this.createAutoChild("deriveForm", {
        fields: [
            {name: "sql", showTitle: false, formItemType: "AutoFitTextAreaItem",
             width: "*", height: 40, colSpan: "*",
             keyPress:function (item, form, keyName) {
                if (keyName == 'Enter' && isc.EH.ctrlKeyDown()) {
                   if (isc.Browser.isSafari) item.setValue(item.getElementValue());
                   _this.execSQL();
                   if (isc.Browser.isSafari) return false;
                }
            }},
            {type: "button", title: "Execute", startRow: true, click: this.getID()+".execSQL()"}
        ]
    });

    /*
    // disabled - would need to add some instructions and error handling before this can be shown
    stack.addSections([isc.addProperties(this.deriveFieldsSectionDefaults,
        this.deriveFieldsSectionProperties,
        { items:[this.deriveForm] }
    )]);

    //this.operationsGrid = this.createAutoChild("operationsGrid");
    //stack.addSection({ID: "operationsSection", title: "Operations", expanded: false, items: [this.operationsGrid]});

    this.previewGrid = this.createAutoChild("previewGrid");
    stack.addSection({ID: "previewSection", title: "Preview", expanded: false, items: [this.previewGrid]});
    */

    stack.addSections({expanded: true, showHeader: false, items: [this.saveButton]});

},

execSQL : function () {
    var sql = this.deriveForm.getValue("sql");
    if (sql) {
        // strip whitespaces and trailing semicolons - these produce a syntax error when passed
        // to the JDBC tier
        sql = sql.trim().replace(/(.*);+/, "$1");
        var ds = isc.DataSource.get("DataSourceStore");
        ds.performCustomOperation("dsFromSQL", {dbName: this.mainEditor.getValue("dbName"), sql: sql}, this.getID()+".deriveDSLoaded(data)");
    }
},

deriveDSLoaded : function (data) {
    var ds = data.ds;
    this.dsLoaded(data.ds);
},

dsLoaded : function (dsConfig) {
    var ds = isc.DataSource.create(dsConfig);
    this.currentDS = ds;

    this.deriveFields(ds);
    this.previewGrid.setDataSource(ds);

    /*
    var ob = ds.operationBindings;
    if (ob && ob.length > 0) {
        this.fetchOperationForm.setValues(ob[0]);
    }
    */
},

deriveFields : function (ds) {
    var fields = ds.getFieldNames();

    var newFields = [];
    for (var i = 0; i < fields.length; i++) {
        var fieldName = fields[i]
        var field = {};
        var dsField = ds.getField(fieldName);
        for (var key in dsField) {
            if (isc.isA.String(key) && key.startsWith("_")) continue;
            field[key] = dsField[key];
        }
        newFields.add(field);
    }

    var tree = isc.Tree.create({
        modelType: "parent",
        childrenProperty: "fields",
        titleProperty: "name",
        idField: "id",
        nameProperty: "id",
        root: { id: 0, name: "root"},
        data: newFields
    });
    this.fieldEditor.setData(tree);
}


});

}
isc._debugModules = (isc._debugModules != null ? isc._debugModules : []);isc._debugModules.push('DataBinding');isc.checkForDebugAndNonDebugModules();isc._moduleEnd=isc._DataBinding_end=(isc.timestamp?isc.timestamp():new Date().getTime());if(isc.Log&&isc.Log.logIsInfoEnabled('loadTime'))isc.Log.logInfo('DataBinding module init time: ' + (isc._moduleEnd-isc._moduleStart) + 'ms','loadTime');delete isc.definingFramework;}else{if(window.isc && isc.Log && isc.Log.logWarn)isc.Log.logWarn("Duplicate load of module 'DataBinding'.");}

/*

  SmartClient Ajax RIA system
  Version v9.1p_2021-05-18/LGPL Deployment (2021-05-18)

  Copyright 2000 and beyond Isomorphic Software, Inc. All rights reserved.
  "SmartClient" is a trademark of Isomorphic Software, Inc.

  LICENSE NOTICE
     INSTALLATION OR USE OF THIS SOFTWARE INDICATES YOUR ACCEPTANCE OF
     ISOMORPHIC SOFTWARE LICENSE TERMS. If you have received this file
     without an accompanying Isomorphic Software license file, please
     contact licensing@isomorphic.com for details. Unauthorized copying and
     use of this software is a violation of international copyright law.

  DEVELOPMENT ONLY - DO NOT DEPLOY
     This software is provided for evaluation, training, and development
     purposes only. It may include supplementary components that are not
     licensed for deployment. The separate DEPLOY package for this release
     contains SmartClient components that are licensed for deployment.

  PROPRIETARY & PROTECTED MATERIAL
     This software contains proprietary materials that are protected by
     contract and intellectual property law. You are expressly prohibited
     from attempting to reverse engineer this software or modify this
     software for human readability.

  CONTACT ISOMORPHIC
     For more information regarding license rights and restrictions, or to
     report possible license violations, please contact Isomorphic Software
     by email (licensing@isomorphic.com) or web (www.isomorphic.com).

*/

