Search code examples
c++builderdomdocumentvclole

MSXML2.DOMDocument.6.0 "MultipleErrorMessages" Property Name is Invalid


I'm trying to use OLE Variants to do XML validation in Embarcadero C++Builder 10.1 Berlin. My ultimate goal is to show all validation errors instead of just the first one (following this MSDN example). My class is below. When I run the following line, I get an exception of "Property name is invalid."

FXmlDomDoc.Exec( XmlFuncSetProperty );

If I comment this line out, everything runs fine.

This makes it seem that "MultipleErrorMessages" is not a valid argument to setProperty() on a MSXML2.DOMDocument.6.0. However, when I look at the list of Second-Level DOM Properties, it seems like this is a valid second-level property for a 6.0 XML DOM object.

What I have tried:

  1. Defining XmlFuncSetProperty as a Procedure instead of a Function; same error.

  2. Setting ValidateOnLoad / ValidateOnParse to false, just in case those somehow affected this; same error.

  3. Re-writing the class using _di_IXMLDOMDocument3, _di_IXMLDOMSchemaCollection2, _di_IXMLDOMParseError. I couldn't find any support for multiple errors in those classes. I did find the functions I needed in a couple of other classes, but they were pure virtual.

Questions:

  • What am I missing here? Why is this error occurring?

  • Is there a better way to do this in C++ Builder?

.cpp file:

//------------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
//------------------------------------------------------------------------------
#include "XmlValidatorU.h"
#include <System.Win.ComObj.hpp>
//------------------------------------------------------------------------------
#pragma package(smart_init)
//------------------------------------------------------------------------------
// Validates XML against Schema
//------------------------------------------------------------------------------
// This class uses OLE objects from MSXML2 to validate XML from an XSD file.
// Generally, use the following steps to deal with OLE objects:
//  1. Define a Variant variable for your OLE Object; assign using CreateOleObject().
//  2. Define your TAutoCmd objects that will be used in Variant.Exec()
//  3. Set TAutoCmd args using << to add settings
//  4. Once everything is set up, call Exec() on your OLE Object variant
// More documentation on OLE objects / TAutoCmd at:
//  http://docwiki.embarcadero.com/CodeExamples/Rio/en/AutoCmd_(C%2B%2B)
//------------------------------------------------------------------------------
// This macro clarifies that we're registering OLE Function names to our defined TAutoCmd variables.
//
#define RegisterAutoCmd( _AutoCmd, _OleFunc ) _AutoCmd( _OleFunc )
//------------------------------------------------------------------------------
// These macros clear AutoCmdArgs before setting them.
// I made these because setting an arg multiple times just stacks them up, changing the function signature.
// Then, OLE throws a "Member Not Found" error because it can't find a function with that signature.
//
#define AutoCmdArg( _AutoCmd, _Arg ) _AutoCmd.ClearArgs(); _AutoCmd << _Arg
#define AutoCmdArgs( _AutoCmd, _Arg1, _Arg2 ) AutoCmdArg( _AutoCmd, _Arg1 ); _AutoCmd << _Arg2
//------------------------------------------------------------------------------
__fastcall TXmlValidator::TXmlValidator( String _SchemaLocation )
    :
    RegisterAutoCmd( CacheProcAdd,              "add"               ),
    RegisterAutoCmd( CacheSetValidateOnLoad,    "validateOnLoad"    ),
    RegisterAutoCmd( XmlProcLoadXml,            "loadXML"           ),
    RegisterAutoCmd( XmlFuncSetProperty,        "setProperty"       ),
    RegisterAutoCmd( XmlSetValidateOnParse,     "validateOnParse"   ),
    RegisterAutoCmd( XmlSetResolveExternals,    "resolveExternals"  ),
    RegisterAutoCmd( XmlSetSchemas,             "schemas"           ),
    RegisterAutoCmd( XmlGetParseError,          "parseError"        ),
    RegisterAutoCmd( ParseErrorGetReason,       "reason"            )
{
    if ( _SchemaLocation.IsEmpty() )
    {
        FInvalidMsg = "No Schema Location Specified";
    }
    else if ( ! FileExists( _SchemaLocation ) )
    {
        FInvalidMsg = "Schema File Does Not Exist: " + _SchemaLocation;
    }
    else
    {
        FInvalidMsg = "";
    }

    if ( FInvalidMsg.Length() > 0 )
    {
        return;
    }

    // Instantiate the OLE objects
    FSchemaCache    = CreateOleObject( "MSXML2.XMLSchemaCache.6.0"  );
    FXmlDomDoc      = CreateOleObject( "MSXML2.DOMDocument.6.0"     );

    // Set static args that shouldn't change
    AutoCmdArg( CacheSetValidateOnLoad, true );
    AutoCmdArg( XmlSetValidateOnParse,  true );
    AutoCmdArg( XmlSetResolveExternals, true );

    AutoCmdArgs( XmlFuncSetProperty, "MultipleErrorMessages", true );

    const AnsiString NoNameSpace = "";
    AutoCmdArgs( CacheProcAdd, NoNameSpace, AnsiString( _SchemaLocation ) );

    // Load Cache
    FSchemaCache.Exec( CacheSetValidateOnLoad   );  // Validate on Load
    FSchemaCache.Exec( CacheProcAdd             );  // Add Schema file location to the cache

    // Now that the cache is loaded, set cached schema as arg to XML
    AutoCmdArg( XmlSetSchemas, FSchemaCache );

    // Set up Xml Dom doc as much as we can...
    FXmlDomDoc.Exec( XmlSetValidateOnParse  );
    FXmlDomDoc.Exec( XmlSetResolveExternals );
    FXmlDomDoc.Exec( XmlSetSchemas          );
    FXmlDomDoc.Exec( XmlFuncSetProperty     );
}
//------------------------------------------------------------------------------
String __fastcall TXmlValidator::ValidationError( String _Xml )
{
    if ( FInvalidMsg.Length() > 0 )
    {
        return FInvalidMsg;
    }

    AutoCmdArg( XmlProcLoadXml, AnsiString( _Xml ) );       // update the XML for re-parsing

    FXmlDomDoc.Exec( XmlProcLoadXml );                      // Load the doc from the XML

    Variant ParseErr = FXmlDomDoc.Exec( XmlGetParseError ); // Get the parseError object

    return ParseErr.Exec( ParseErrorGetReason );            // Extract the reason
}
//------------------------------------------------------------------------------

.h file:

//------------------------------------------------------------------------------
#ifndef XmlValidatorUH
#define XmlValidatorUH
//------------------------------------------------------------------------------
class PACKAGE TXmlValidator
{
private:
    String  FInvalidMsg;

    // OLE Variant Variables
    Variant FSchemaCache;
    Variant FXmlDomDoc;

    // TAutoCmd Variables
    Procedure   CacheProcAdd;
    PropertySet CacheSetValidateOnLoad;
    Procedure   XmlProcLoadXml;
    Function    XmlFuncSetProperty;
    PropertySet XmlSetValidateOnParse;
    PropertySet XmlSetResolveExternals;
    PropertySet XmlSetSchemas;
    PropertyGet XmlGetParseError;
    PropertyGet ParseErrorGetReason;

public:
    __fastcall TXmlValidator( String _SchemaLocation );

    String __fastcall ValidationError( String _Xml );

};
//------------------------------------------------------------------------------
#endif

Solution

  • Thanks to Remy's excellent advice, I got this working in Debug mode. However, I found that AutoCmd caused an Access Violation in the Release build with this code. Therefore, for completeness, I've included my final answer below (that works in both Debug and Release). It abandons AutoCmd for OLEVariant methods.

    .cpp file:

    //------------------------------------------------------------------------------
    #include <vcl.h>
    #pragma hdrstop
    //------------------------------------------------------------------------------
    #include "XmlValidatorU.h"
    #include <System.Win.ComObj.hpp>
    //------------------------------------------------------------------------------
    #pragma package(smart_init)
    //------------------------------------------------------------------------------
    // This class uses OLE objects from MSXML2 to validate XML from an XSD file.
    // Generally, use the following steps to deal with OLE objects:
    //  1. Define a Variant variable for your OLE Object; assign using CreateOleObject()
    //  2. Use Variant.OlePropertySet(), OlePropertyGet(), OlePropertySet(), OleProcedure(),
    //      and OleFunction() to do actions on your OLE objects
    //  3. Previously, I had this working with TAutoCmd arguments, which worked great
    //      in Debug, but then yielded a mysterious Access Violation in Release build.
    //
    // Even though it poops its pants in Release, here's documentation of Embarcadero's AutoCmd solution:
    //  http://docwiki.embarcadero.com/CodeExamples/Rio/en/AutoCmd_(C%2B%2B)
    //
    // See MSDN tutorial for allErrors here: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms754649%28v%3dvs.85%29
    //
    //------------------------------------------------------------------------------
    // This macro casts char* strings to WideString
    #define wstr( _CharPtr ) WideString( L##_CharPtr )
    //------------------------------------------------------------------------------
    __fastcall TXmlValidator::TXmlValidator( String _SchemaLocation )
    {
        if ( _SchemaLocation.IsEmpty() )
        {
            FInvalidMsg = "No Schema Location Specified";
        }
        else if ( ! FileExists( _SchemaLocation ) )
        {
            FInvalidMsg = "Schema File Does Not Exist: " + _SchemaLocation;
        }
        else
        {
            FInvalidMsg = "";
        }
    
        if ( FInvalidMsg.Length() > 0 )
        {
            return;
        }
    
        // First, set up the MSXML2.XMLSchemaCache.6.0 OLE object
        FSchemaCache = CreateOleObject( wstr( "MSXML2.XMLSchemaCache.6.0" ) );
        FSchemaCache.OlePropertySet( _D( "validateOnLoad" ), true );
        FSchemaCache.OleProcedure( _D( "add" ), wstr( "" ), WideString( _SchemaLocation ) );
    
        // Now set up the MSXML2.DOMDocument.6.0 OLE object
        FXmlDomDoc = CreateOleObject( wstr( "MSXML2.DOMDocument.6.0" ) );
        FXmlDomDoc.OlePropertySet( _D( "validateOnParse"  ),  true          );
        FXmlDomDoc.OlePropertySet( _D( "resolveExternals" ),  true          );
        FXmlDomDoc.OlePropertySet( _D( "schemas"          ),  FSchemaCache  );  // set schemas to the cache we created above
        FXmlDomDoc.OleProcedure( _D( "setProperty" ), wstr( "MultipleErrorMessages" ), true ); // secondary properties must call setProperty()
    }
    //------------------------------------------------------------------------------
    // This function checks _ParseError for a non-zero error code.
    // If found, add Reason and Location to _Result. If no Reason found, at least add Error Code.
    void __fastcall TXmlValidator::AddErrorDesc( String& _Result, Variant _ParseError )
    {
        long TmpErrCode = _ParseError.OlePropertyGet( _D( "errorCode" ) );
    
        if ( 0 == TmpErrCode )
        {
            return;
        }
    
        String TmpReason = _ParseError.OlePropertyGet( _D( "reason" ) );
        String TmpXPath  = _ParseError.OlePropertyGet( _D( "errorXPath" ) );
    
        TmpReason = TmpReason.Trim();
        TmpXPath  = TmpXPath.Trim();
    
        if ( TmpReason.IsEmpty() )
        {
            TmpReason = "Error Code: " + String( TmpErrCode );
        }
        else
        {
            TmpReason = "Reason: " + TmpReason;
        }
    
        if ( TmpXPath.Length() > 0 )
        {
            TmpXPath = StringReplace( TmpXPath, "[1]", "", TReplaceFlags() << rfReplaceAll );
            TmpXPath = "Location: " + TmpXPath;
        }
    
        _Result += TmpReason + "\r\n\r\n";
        _Result += TmpXPath + "\r\n";
    }
    //------------------------------------------------------------------------------
    String __fastcall TXmlValidator::ValidationError( String _Xml )
    {
        String Result = FInvalidMsg;
    
        if ( Result.Length() > 0 )
        {
            return Result;
        }
    
        FXmlDomDoc.OleProcedure( _D( "loadXML" ), WideString( _Xml ) );
    
        // No need to call AddErrorDesc for TopParseErr; it duplicates the first item in allErrors
        Variant TopParseErr = FXmlDomDoc.OlePropertyGet( _D( "parseError" ) );
        Variant AllErrors   = TopParseErr.OlePropertyGet( _D( "allErrors" ) );
        int TmpAllErrLength = AllErrors.OlePropertyGet( _D( "length" ) );
    
        for ( int ix = 0; ix < TmpAllErrLength; ix++ ) // Iterate through allErrors
        {
            Variant TmpErrItem = AllErrors.OlePropertyGet( _D( "item" ), ix );
    
            AddErrorDesc( Result, TmpErrItem ); // Add error desc
        }
    
        return Result;
    }
    //------------------------------------------------------------------------------
    

    .h file:

    //------------------------------------------------------------------------------
    #ifndef XmlValidatorUH
    #define XmlValidatorUH
    //------------------------------------------------------------------------------
    class PACKAGE TXmlValidator
    {
    private:
        String  FInvalidMsg;
        Variant FSchemaCache;
        Variant FXmlDomDoc;
    
        void __fastcall AddErrorDesc( String& _Result, Variant _ParseError );
    
    public:
        __fastcall TXmlValidator( String _SchemaLocation );
    
        String __fastcall ValidationError( String _Xml );
    
    };
    //------------------------------------------------------------------------------
    #endif