SSRS, RS UTILITY AND OLE AUTOMATION

The ability to full justify is a functionality that has never existed in SSRS since its inception and apparently Microsoft is still not supporting it. After much trial and error, I have come up with a workaround that hopefully will help a lot of you guys out there.

Prerequisite:

  • MS Word has to be installed in the same server as Reporting Services.
  • OLE Automation procedures need to be enabled.
  • xp_cmdshell needs to be enabled.
  • SQL Server Service Account needs to be added to the list of Administrators in the Windows UAC.
Disadvantages of this method:
  • This workaround cannot be used if more than one user is simultaneously trying to export the SAME report.
  • Difficult to trace if a COM object failure occurs. Usually the only solution is a Server restart. (But this rarely happens)

1) TO ENABLE OLE AUTOMATION PROCEDURES, EXECUTE THE FOLLOWING COMMANDS:

sp_configure 'show advanced options', 1
GO
RECONFIGURE;
GO
sp_configure 'Ole Automation Procedures', 1
GO
RECONFIGURE;

GO

2) TO ENABLE XP_CMDSHELL, EXECUTE THE FOLLOWING COMMANDS:

EXEC sp_configure 'show advanced options', 1
GO
RECONFIGURE
GO
EXEC sp_configure 'xp_cmdshell', 1
GO
RECONFIGURE

GO

3) CREATE THE FOLLOWING STORED PROCEDURE BY EXECUTING THE BELOW SCRIPT AGAINST THE ADVENTURE WORKS 2012 DATABASE:

{AdventureWorks2012 database can be downloaded here:
https://msftdbprodsamples.codeplex.com/releases/view/55330 }



################################################################################
USE [AdventureWorks2012]
GO

/****** Object:  StoredProcedure [dbo].[usp_FullJustify]    Script Date: 3/17/2015 11:05:25 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

create PROCEDURE [dbo].[usp_FullJustify]
       @RDLfilepath                     VARCHAR(200),  -- Path of the RDL File
       @ScriptFileLocation              VARCHAR(200),  -- Local file location for the RSS Script file
       @DestinationFilePath             VArchar(200),  -- Local file location for the RDL Export
       @DestinationFileName             VARCHAR(100),  -- RDL Export File Name
       @ReportServer                    VARCHAR(100),  -- Usually in the format :http://<ServerName>/ReportServer
       @WSDL                            VARCHAR(100)   -- Usually in the format :http://<ServerName>/ReportServer/ReportExecution2005.asmx?wsdl
      
      
AS


SET NOCOUNT ON


/************************************************************************************
       Locals
************************************************************************************/
DECLARE @CmdOutput TABLE (
             ID                         INT IDENTITY(1,1),
             OutputString VARCHAR(1000) )
DECLARE     
             @ReturnVal                       INT,
             @ThisProcName              VARCHAR(100),
             @StepDescription           VARCHAR(MAX),
             @StepResult                      VARCHAR(200),
             @Error                           INT,
             @ErrorMessage              VARCHAR(500),
             @TempFile                        varchar(500),
             @DocFile                         varchar(300),
             @Cmd                             varchar(8000),
             @ExecutedBy                      varchar(100) = suser_sname()

       SET    @ReturnVal                       = 0
       SET @ThisProcName                = ISNULL(OBJECT_NAME(@@PROCID), 'usp_FullJustify')
       SET    @ErrorMessage              = ''
       SET @Error                              = 0
      


--VARIOUS FORMATS IN WHICH DOC FILES CAN BE SAVED. CHOOSE BASED ON REQUIREMENT
--****************************************************************************
    declare @wdFormatDocument int                                  =  0
    declare @wdFormatDocument97 int                                      =  0
    declare @wdFormatDocumentDefault int                           = 16
    declare @wdFormatDOSText int                                   =  4
    declare @wdFormatDOSTextLineBreaks int                         =  5
    declare @wdFormatEncodedText int                               =  7
    declare @wdFormatFilteredHTML int                              = 10
    declare @wdFormatFlatXML int                                   = 19
    declare @wdFormatFlatXMLMacroEnabled int                = 20
    declare @wdFormatFlatXMLTemplate int                           = 21
    declare @wdFormatFlatXMLTemplateMacroEnabled int = 22
    declare @wdFormatHTML int                                            =  8
    declare @wdFormatPDF int                                             = 17
    declare @wdFormatRTF int                                             =  6
    declare @wdFormatTemplate int                                  =  1
    declare @wdFormatTemplate97 int                                      =  1
    declare @wdFormatText int                                            =  2
    declare @wdFormatTextLineBreaks int                            =  3
    declare @wdFormatUnicodeText int                               =  7
    declare @wdFormatWebArchive int                                      =  9
    declare @wdFormatXML int                                             = 11
    declare @wdFormatXMLDocument int                               = 12
    declare @wdFormatXMLDocumentMacroEnabled int            = 13
    declare @wdFormatXMLTemplate int                               = 14
    declare @wdFormatXMLTemplateMacroEnabled int            = 15
    declare @wdFormatXPS int                                             = 18

IF object_id('tempdb..##STAGE') IS NOT NULL
BEGIN
       DROP TABLE ##STAGE
END
      
CREATE TABLE ##STAGE
    (Command VARCHAR(8000));

BEGIN TRY
       INSERT INTO ##STAGE
       VALUES
       (
       'public Sub main()

rs.Credentials = System.Net.CredentialCache.DefaultCredentials
rs.Url = "'+ @WSDL +'"
Dim execHeader AS New ExecutionHeader()
Dim ReportPath As String = "'+@RDLfilepath +'"
rs.ExecutionHeaderValue = execHeader

rs.LoadReport(ReportPath, Nothing)
Dim Format As String = "Word"
Dim devInfo As String = Nothing
Dim extension As String = Nothing
Dim mimeType As String = "application\msword"
Dim encoding As String = Nothing
Dim warnings() AS Warning = Nothing
Dim stremIds() As String = Nothing
Dim result() As Byte = Nothing

result = rs.Render(Format, devInfo, extension, mimeType, encoding, warnings, stremIds)

Dim stream as filestream = file.create("'+@DestinationFilePath +@DestinationFileName +'",result.length)
stream.write(result,0,result.Length)
stream.close()

End Sub'
             )     
       END TRY
       BEGIN CATCH
             SET @Error                 = @@ERROR
             SET @StepResult            = ERROR_MESSAGE()
             GOTO ENDOFPROC
       END CATCH
         
    
    SET @TempFile = @ScriptFileLocation  + 'ExecuteRDL.rss'
    SET @Cmd  = 'bcp "SELECT COMMAND FROM ##STAGE" queryout ' + @TempFile + ' -T  -c'

    BEGIN TRY             
             EXEC @ReturnVal = master..xp_cmdShell @Cmd
       END TRY
       BEGIN CATCH
             SET @Error                 = @@ERROR
             SET @StepResult            = ERROR_MESSAGE()
             GOTO ENDOFPROC
       END CATCH
      

SET @cmd = 'rs -i ' + @TempFile  + ' -s ' + @ReportServer + ' -e exec2005'
-- Call RDL
BEGIN TRY
       DELETE FROM @CmdOutput
             INSERT @CmdOutput
       EXEC @ReturnVal = master..xp_cmdShell @cmd--, NO_OUTPUT
END TRY     
BEGIN CATCH
      
       SET @Error                 = @@ERROR
       SET @StepResult            = ERROR_MESSAGE()
       GOTO ENDOFPROC
END CATCH

IF @ReturnVal != 0 BEGIN
       --Attempt to get the errors from @CMDOutput by concatenating rows
              
             SET @StepResult = NULL
             SELECT TOP 2
                    @StepResult = COALESCE(@StepResult + ' || ', '') + CAST(RTRIM(co.OutputString) AS VARCHAR(MAX))
             FROM
                    @CmdOutput co
             WHERE
                    co.OutputString IS NOT NULL
             ORDER BY
                    co.ID DESC
            
             select * from @CmdOutput

             SET @StepResult            =ISNULL(@StepResult, 'ERROR: ReturnVal = ' + CAST(@ReturnVal AS VARCHAR))
             GOTO ENDOFPROC
END


--Instantiate OLE Automation Environment with MS Word
DECLARE @WordObject INT, @objdocument INT, @objDoc INT,@RetCode INT, @Document INT , @Filename VARCHAR(255)

EXEC @RetCode = sp_OACreate 'Word.Application', @WordObject OUTPUT, 4
       IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler

EXEC @RetCode = sp_OASetProperty @WordObject,'Visible', 0
       IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler


SET @DocFile = @DestinationFilePath + @DestinationFileName

EXEC @RetCode = sp_OAGetProperty @WordObject,
                    'Documents.open',@objDocument OUTPUT, @DocFile
                          
IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler
      
EXEC @RetCode = sp_OAMethod @WordObject,
             'Selection.WholeStory'
IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler           

EXEC @RetCode = sp_OASetProperty @WordObject,
             'Selection.ParagraphFormat.Alignment', 3
IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler
                   
SET @DestinationFilePath  = REPLACE (@DestinationFilePath ,'.doc','.pdf')

EXEC @RetCode = sp_OAMethod @WordObject,
             'ActiveDocument.SaveAs' , NULL , @DestinationFilePath , @wdFormatPDF
                          
IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler           

EXEC @RetCode = sp_OAMethod @objDocument,
             'Close'
IF @RetCode <> 0 or @@Error <> 0 GOTO OLE_Error_Handler    
                   
EXEC sp_OADestroy @objDocument
                   
                          

-- Quit Word and destroy OLE Object
EXEC @RetCode = sp_OAMethod @WordObject, 'Quit'
EXEC sp_OADestroy @WordObject

-- Delete existing doc files
SET @StepDescription = 'Delete any doc files present after full justification'
SET @cmd = 'del ' + @DestinationFilePath +'*.doc'

BEGIN TRY
       EXEC @ReturnVal = master..xp_cmdShell @cmd, NO_OUTPUT
       --exec xp_cmdshell @cmd
END TRY     
BEGIN CATCH
       SET @Error                 = @@ERROR
       SET @StepResult            = ERROR_MESSAGE()
       GOTO ENDOFPROC
END CATCH

IF @ReturnVal != 0 BEGIN
       SET @StepResult            ='ERROR: ReturnVal = ' + CAST(@ReturnVal AS VARCHAR)
       GOTO ENDOFPROC
END


GOTO Done

/************************************************************************************
       Log errors if applicable
************************************************************************************/
ENDOFPROC:
             SET @ErrorMessage = @StepResult + ' at ' + @StepDescription
             --RAISERROR(50001, @ErrorSeverity, @ErrorState, @ThisProcname, @ErrorMessage)
             --EXEC dbo.usp_LetterLogAdd
                    SELECT 'Error At
                    '+@ThisProcName + '; ExecutedBy '+@ExecutedBy +
                    '; StepDescription --' +@StepDescription
                    +'; StepResult --'+@StepResult
                    +'; ErrorMessageID --'+convert(varchar,@Error)
                   
       GOTO Done


/************************************************************************************
       Capture Error Information from OLE Automation if applicable
************************************************************************************/
OLE_Error_Handler:

       --Exec sp_displayoaerrorinfo @WordObject, @RetCode
       DECLARE             @output varchar(255),
                           @hr int,
                           @source varchar(255),
                           @description varchar(255)
      
       PRINT 'OLE Automation Error Information'
       EXEC @hr = sp_OAGetErrorInfo @WordObject, @source OUT, @description OUT
       IF @hr = 0
       BEGIN
             SELECT @output = '  Source: ' + @source
             PRINT @output
             SELECT @output = '  Description: ' + @description
             PRINT @output
       END
       ELSE
       BEGIN
             SELECT @output = '  sp_OAGetErrorInfo failed.'
             PRINT @output
             RETURN
       END

       SET @StepDescription = 'OLE Automation Failed'

       EXEC @RetCode = sp_OAMethod @WordObject, 'Quit'
       EXEC sp_OADestroy @WordObject

       SET @Error                 = @RetCode
       SET @StepResult            = 'Source: ' + @source + '; Description: ' + @description
      
       SELECT 'Error At
                    '+@ThisProcName + '; ExecutedBy '+suser_sname()+
                    '; StepDescription --' +@StepDescription
                    +'; StepResult --'+@StepResult
                    +'; ErrorMessageID --'+convert(varchar,@Error)
                   
       GOTO Done


/************************************************************************************
       Delete script file and stop process
************************************************************************************/
Done:
            
       -- Delete rss file
       SET @cmd = 'del ' + @ScriptFileLocation + '*.rss'
       EXEC xp_cmdshell @cmd
      
      
GO


##################################################################################

What the above procedure does is it's creating an RSS script file initially to access the RDL at Report Manager and exporting it to a .doc file. Then using OLE Automation, we're establishing a connection to Word to operate the .doc file in the background whilst making use of Word's capability to full justify the document and then exporting it any format we desire.

In this example, we're exporting it to a PDF file. But you can always change this to any format you desire by making use of this stored procedure and the available formats mentioned in it.

The change has to be done at this step :
EXEC @RetCode = sp_OAMethod @WordObject,
             'ActiveDocument.SaveAs' , NULL , @DestinationFilePath , @wdFormatPDF

Instead of @wdFormatPDF, you can use @wdFormatRTF, @wdFormatXPS, @wdFormatHTML and by specifying the mime type. 


 4)NOW WE GO AHEAD AND CREATE A REPORT

 I won't go in detail here on how to develop a report. I am sure you can find that in many other blogs.. :). So I am providing a link to a sample RDL here which you can use. It looks like this in BIDS:


Once you have this in your BIDS project, you can deploy this by clicking on Build at the top and clicking on Deploy. You may need to set the Deployment properties for the Project before you do this.
Or you can directly go to Report Manager and upload the RDL files attached along with the Data Source. In this case as you can see, I have uploaded to the Report Manager Path :- Home --> AdventureWorks --> Reports --> Sales Order Header













The last step here is to ensure that the RDL points to the right data source. You can ensure this by clicking on the Manage tab of the RDL and browsing over to Data Sources Tab. Select the appropriate Data Source and click Apply.


























5) GO BACK TO SSMS AND EXECUTE THE STORED PROC

A Sample execution would be: 

exec [dbo].[usp_FullJustify]
       @RDLfilepath='/AdventureWorks/Reports/Sales Order Header',
       @ScriptFileLocation ='C:\SSRSTesting\',
       @DestinationFilePath='C:\SSRSTesting\',
       @DestinationFileName='Sales Order Receipt.doc',
       @ReportServer='http://manoj-pc/ReportServer',
       @WSDL='http://manoj-pc/ReportServer/ReportExecution2005.asmx?wsdl'



As and when the query executes you can see how the script gets created, the doc file gets exported and finally gets converted to a PDF Full Justified.









And there you have it. A fully automated way of exporting SSRS reports FULL JUSTIFIED. Pretty neat huh !!.. :)

Happy Reporting..!!!