In the previous articles, we gradually got acquainted with the framework classes, and we saw how to create our own classes, based on the classes supplied. In this article, we will see the details of handling and controlling errors, provided by the framework.
Remember that you can get the latest version of TierAdapter and the complete Demo application, including source code, from our framework's main page at SourceForge.
Error handling
Error handling in TierAdapter is done using the Try-Catch structure. For those of you who are not familiar with this method of handling errors, I recommend you read the excellent article written by Antonio Castaño in the UTMag edition from October 2002, where he explains the main features of this structure.
The way to react to an error goes beyond the reach of the framework, therefore, we won't discuss it in the present article, but we want to explain the main tools available for controlling them.
The basic idea is to place, within a Try/Catch block, a call to any process that might generate an exception (is there any process that can't?). When the exception is generated, it will be trapped by the Catch command, and processed differently, depending on the tier where it happens. The tier which is closest to the database engine, which is not responsible for the user interface, traps the exception and saves the information into a cursor called cExceptionInformation. Finally, this cursor is serialized as XML and sent to the tier immediately above it, as a result of the requested process. Let's see an example of how this would work at the data tier level.
Errors in the data tier
Function GetAllEmailAddress() As String Local llAlreadyConnected As Boolean Local loMyCA AS rrCursorAdapter Try llAlreadyConnected = Not This.ConnectToBackend() loMyCA = NewObject( "rrCursorAdapter", ; "rrCursorAdapter.Prg", ; "", ; This.cAccessType, ; This.oADOConnection ) With loMyCA .Alias = "cAllEmails" .SelectCmd = "Select cCompanyName, cEmail form Customers" .CursorFill() .CursorDetach() EndWith Catch To loError loMyError = NewObject( "rrException", "rrException.prg" ) loMyError.Save( loError ) Finally loMyCA = .F. Release loMyCA If Not llAlreadyConnected This.DisconnectFromBackend() EndIf EndTry Return ( This.Serialize( "cAllEmails" ) ) EndFunc
As an example, this method returns all e-mail addresses from a table called Customers. For this purpose, within the TRY block, it tries to connect to the database engine, and through a CursorAdapter, extract the required data. In case there is an error in any of the operations involved, the exception generated will be caught by the CATCH, into an exception object called loError. Then, within the same CATCH block, an object rrException is created (see rrException.prg), invoking its Save() method, passing, as a parameter, the exception recently trapped. This method is in charge of storing the data about the error in the cursor cExceptionInformation, which was created when the rrException object was created. Following with the FINALLY block, which gets executed whether or not there is an error, we see that the method destroys the CursorAdapter created previously, and connects to the database engine. At the end of the activities of the analized method, we see that it returns the result of the call to method Serialize() that verifies the existence of the cursor cExceptionInformation. If it exists, its content is converted to XML. In case it doesn't exist, what is passed to XML are the data of the cursors whose names have been received as parameters.
Before we go ahead and see how the receiving tier of this XML behaves, let's review the code of some of the methods named in the previous paragraph.
The class rrException
Let's see some of the methods of the class rrException, which, as we can see in the following declaration, is a specialization of Visual FoxPro's native Exception class.
DEFINE CLASS rrException AS Exception
The Init method is responsible for creating the cursor where we are going to save the error information.
Procedure Init If Not Used( "cExceptionInformation" ) Create Cursor cExceptionInformation (; Details Varchar(250),; ErrorNo Integer,; ErrorLevel Varchar(50),; LineContents Varchar(250),; LineNo Integer,; Message Varchar(250),; Procedure Varchar(250),; StackLevel Integer,; cStackInfo Memo ) EndIf EndProc
The Save method is responsible for extracting the information from the exception received as a parameter, and inserts it into the cursor cExceptionInformation.
Procedure Save( toError AS Object ) AS Void . . . WITH This IF VarType( toError ) = "O" DO WHILE VarType( toError.UserValue ) = "O" toError = toError.UserValue EndDo .Details = toError.Details .ErrorNo = toError.ErrorNo .LineContents = toError.LineContents .LineNo = toError.LineNo .Message = toError.Message .Procedure = toError.Procedure .StackLevel = toError.StackLevel EndIf EndWith Insert Into cExceptionInformation From Name This This.LogError() EndProc
The LogError method is responsible of saving information about the error into a file Log.Err. For this purpose it uses other methods, which you can see in detail in rrException.prg, whose purpose, basically, is to complement the logged information.
Procedure LogError() Local lcError AS String lcError = Replicate( "*", 80 ) + CR ; + [Date: ] + Alltrim( Transform( Date(), "@D" ) ) + CR ; + [Time: ] + Alltrim( Time() ) + CR ; + [UserName: ] + Alltrim( This.GetUser() ) + CR ; + This.GenErrorInfo() + CR ; + Replicate( "*", 80 ) + CRLF StrToFile( lcError, "Log.err", 1 ) EndProc
The Throw method is responsible for firing the exception.
Procedure Throw() Throw This EndProc
The Serialize method
In this simple method we see how, before serializing, the existence of the cursor containing the error information is checked, and it is then serialized. In case it doesn't exist, the cursors are serialized whose names are provided as method parameters, in a comma-delimited list.
Function Serialize( tcCommaSeparatedCursorList As String ) As String Local loMyXA As rrXMLAdapter, lcRetVal As String loMyXA = Newobject( "rrXMLAdapter", "rrXMLAdapter.Prg" ) If Used( "cExceptionInformation" ) loMyXA.AddTableSchema( "cExceptionInformation" ) Else Local i As Integer For i = 1 To Getwordcount( tcCommaSeparatedCursorList, [,] ) loMyXA.AddTableSchema( Alltrim( ; Getwordnum( tcCommaSeparatedCursorList, i, [,] ) )) Endfor Endif loMyXA.PreserveWhiteSpace = .T. loMyXA.ToXML( "lcRetVal" ) loMyXA = .F. Release loMyXA Return lcRetVal EndFunc
Errors in the business tier
The first thing the receiving tier does is to de-serialize these data through the GetData method, which, after recognizing the presence of the cursor cExceptionInformation, creates an exception, populates the data with which it extract the cursor, and triggers it automatically, using the Throw command.
Here, the same method returns us the e-mail address from the Customers table, at the level of the business tier.
Function GetAllEmailAddress() As String Local lcXML As String Try lcXML = This.oEntidad.GetAllEmailAddress() This.GetData(lcXML ) * Más procesos Catch To loError loMyError = NewObject( "rrException", "rrException.prg" ) loMyError.Save( loError ) Finally EndTry Return ( This.Serialize( "cAllEmails" ) ) EndFunc
The XML returned by the data tier is de-serialized by the GetData method. Then, it is processed according to need at this level. The way to trap any error is similar to the previous tier. Let's see the (simplified) code of the GetData method.
Function GetData( tcData As String ) As Boolean Local loMyXA As rrXMLAdapter loMyXA = Newobject( "rrXMLAdapter", "rrXMLAdapter.Prg" ) loMyXA.LoadXML( tcData, .F. ) For Each oTable In loMyXA.Tables oTable.ToCursor( .F. ) Endfor loMyXA = .F. If Used( "cExceptionInformation" ) loError = Newobject( "rrException", "rrException.prg" ) With loError .Fill() .Throw() Endwith Endif Return .T. EndFunc
The data received as parameters in XML format are converted to cursors. Then, a verification is done, between the generated cursors, for the existence of our error cursor. In case it exists, an exception is created, it is "filled" with data extracted from this cursor, and it is "thrown" so that it may be trapped by the Catch of method GetAllEmailAddress, which, in turn, traps it and sends the corresponding information to the user tier, following the same method as we have seen in the data tier.
Errors in the user tier
In this simple manner, we "raised" the error from one tier to the next. Repeating this procedure in a chain, when we come to the upper tier, the user should be notified that the exception occurred. This notification should be clear, giving the user a range of options to follow to overcome the difficulty.
In TierAdapter, the generation of information to be given to the user in in charge of the program ParseError.prg. Unfortunately, we have not yet implemented this latter one in a class, so that it can be subclassed and specialized according to the needs of every environment, but I assure you that this is one of the priorities of our extensive "To-Do List".
Let's see the code of our method GetAllEmailAddress in the user tier, and then, how ParseError works to generate the error message.
Function GetAllEmailAddress() As Boolean LOCAL loEntidad As Object,; llRetVal As Boolean,; lcXML As String Try lcXML = loEntidad.GetAllEmailAddress() This.GetData(lcXML ) * Más procesos llRetVal = .T. Catch To loError llRetVal = .F. Stop( ParseError( loError ) ) Finally EndTry Return llRetVal EndFunc
ParseError.prg
LOCAL loError AS Object loError = toException DO WHILE loError.ErrorNo = 2071 * User Thrown Error IF VARTYPE( loError.UserValue ) = "O" loError = loError.UserValue ELSE EXIT ENDIF ENDDO DO CASE Case Between(loError.ErrorNo,9990,9999) * Error generated by the user. Return Alltrim( loError.Message ) + CR ; + Alltrim( loError.Details ) Case loError.ErrorNo = 1884 * The uniqueness of primary or candidate key is violated. Local lcField As String lcField = ALLTRIM( loError.Details ) Return CR ; + "Ha intentado ingresar un valor ya existente" + CR ; + "en " + UPPER( RIGHT( lcField, LEN( lcField ) ) ) + "." + CR ; + CR + "Los datos no han sido guardados." OTHERWISE Return CR ; + [Error: ] + ALLTRIM( Transform(loError.ErrorNo) ) + CR ; + [Message: ] + ALLTRIM( loError.Message ) + CR ; + [Procedure: ] + ALLTRIM( loError.Procedure ) + CR ; + [LineNo: ] + ALLTRIM( Transform( loError.LineNo) ) + CR ; + [Details: ] + ALLTRIM( loError.Details ) + CR ; + [StackLevel: ] + ALLTRIM( Transform( loError.StackLevel) ) + CR ; + [LineContents: ] + ALLTRIM( loError.LineContents ) EndCase
The initial loop within ParseError searches the original exception that caused the chain of exceptions that finally generated the successive Throws to reach the upper tier. The following DO CASE lets us differentiate the behavior to follow for different types of errors. The general behavior followed by the OTHERWISE takes care of concatenating the error data in a formatted string to be exposed to the user through Stop (see stop.prg).
Conclusion
Today we saw how to control errors that can be generated at different tiers of our application. For lack of space, we will still be in debt, with regards to our promise, in the previous issue, about managing transactions, to which we will dedicate the entire next issue. See you next month.