Showing posts with label Java. Show all posts
Showing posts with label Java. Show all posts

Tuesday, 22 January 2019

VBA - Sockets - Ruby - Java - Interop Nirvana with Sockets!

I hope you have noticed that inter-operability features large on this blog. Interop is easy with C# because it has an excellent COM interop library. Python and other languages also have COM APIs (not always as comprehensive as C#). But Java and Ruby do not have COM APIs callable from VBA. But this month we have shown that it is possible to use Sockets to connect with other processes (in the demos on the same machine but in real life potentially on remote machines). Thus, using sockets Excel VBA call anything, literally anything (so long the target langauge has a sockets API).

In the previous two articles I wrote sockets servers in Ruby then Java which ran simple calculations. They were just warm-up and test-beds to be honest. What I really wanted to show was VBA calling Ruby and Java.

So in the code below if you run the procedure TestWS2SendAndReceive() at line 159 then you can see the code will attempt to call the Java and Ruby servers, make sure you have these running. If all works then you should get output in the Immediate window

1+3 = 4.0
6*7 = 42.0

modVBASocketsClient Standard Module

  1. Option Explicit
  2.  
  3. Option Private Module
  4.  
  5.  
  6. 'reference Windows Sockets 2 - Windows applications _ Microsoft Docs
  7. 'http://msdn.microsoft.com/en-us/library/windows/desktop/ms740673(v=vs.85).aspx
  8. Private Const INVALID_SOCKET = -1
  9. Private Const WSADESCRIPTION_LEN = 256
  10. Private Const SOCKET_ERROR As Long = -1 'const #define SOCKET_ERROR            (-1)
  11.  
  12. Private Enum AF
  13.     AF_UNSPEC = 0
  14.     AF_INET = 2
  15.     AF_IPX = 6
  16.     AF_APPLETALK = 16
  17.     AF_NETBIOS = 17
  18.     AF_INET6 = 23
  19.     AF_IRDA = 26
  20.     AF_BTH = 32
  21. End Enum
  22.  
  23. Private Enum sock_type
  24.     SOCK_STREAM = 1
  25.     SOCK_DGRAM = 2
  26.     SOCK_RAW = 3
  27.     SOCK_RDM = 4
  28.     SOCK_SEQPACKET = 5
  29. End Enum
  30.  
  31. Private Enum Protocol
  32.     IPPROTO_ICMP = 1
  33.     IPPROTO_IGMP = 2
  34.     BTHPROTO_RFCOMM = 3
  35.     IPPROTO_TCP = 6
  36.     IPPROTO_UDP = 17
  37.     IPPROTO_ICMPV6 = 58
  38.     IPPROTO_RM = 113
  39. End Enum
  40.  
  41. 'Private Type sockaddr
  42. '    sa_family As Integer
  43. '    sa_data(0 To 13) As Byte
  44. 'End Type
  45.  
  46. Private Type sockaddr_in
  47.     sin_family As Integer
  48.     sin_port(0 To 1) As Byte
  49.     sin_addr(0 To 3) As Byte
  50.     sin_zero(0 To 7) As Byte
  51. End Type
  52.  
  53.  
  54. 'typedef UINT_PTR        SOCKET;
  55. Private Type udtSOCKET
  56.     pointer As Long
  57. End Type
  58.  
  59.  
  60.  
  61. ' typedef struct WSAData {
  62. '  WORD           wVersion;
  63. '  WORD           wHighVersion;
  64. '  char           szDescription[WSADESCRIPTION_LEN+1];
  65. '  char           szSystemStatus[WSASYS_STATUS_LEN+1];
  66. '  unsigned short iMaxSockets;
  67. '  unsigned short iMaxUdpDg;
  68. '  char FAR       *lpVendorInfo;
  69. '} WSADATA, *LPWSADATA;
  70.  
  71. Private Type udtWSADATA
  72.     wVersion As Integer
  73.     wHighVersion As Integer
  74.     szDescription(0 To WSADESCRIPTION_LEN) As Byte
  75.     szSystemStatus(0 To WSADESCRIPTION_LEN) As Byte
  76.     iMaxSockets As Integer
  77.     iMaxUdpDg As Integer
  78.     lpVendorInfo As Long
  79. End Type
  80.  
  81. 'int errorno = WSAGetLastError()
  82. Private Declare Function WSAGetLastError Lib "Ws2_32" () As Integer
  83.  
  84. '   int WSAStartup(
  85. '  __in   WORD wVersionRequested,
  86. '  __out  LPWSADATA lpWSAData
  87. ');
  88. Private Declare Function WSAStartup Lib "Ws2_32" _
  89.     (ByVal wVersionRequested As IntegerByRef lpWSAData As udtWSADATA) As Long 'winsockErrorCodes2
  90.  
  91.  
  92. '    SOCKET WSAAPI socket(
  93. '  __in  int af,
  94. '  __in  int type,
  95. '  __in  int protocol
  96. ');
  97.  
  98. Private Declare Function ws2_socket Lib "Ws2_32" Alias "socket" _
  99.     (ByVal AF As LongByVal stype As LongByVal Protocol As LongAs LongPtr
  100.  
  101. Private Declare Function ws2_closesocket Lib "Ws2_32" Alias "closesocket" _
  102.     (ByVal socket As LongAs Long
  103.  
  104. 'int recv(
  105. '  SOCKET s,
  106. '  char   *buf,
  107. '  int    len,
  108. '  int    flags
  109. ');
  110. Private Declare Function ws2_recv Lib "Ws2_32" Alias "recv" _
  111.     (ByVal socket As LongByVal buf As LongPtr,
  112.      ByVal length As LongByVal flags As LongAs Long
  113.  
  114. 'int WSAAPI connect(
  115. '  SOCKET         s,
  116. '  const sockaddr *name,
  117. '  int            namelen
  118. ');
  119.  
  120. Private Declare Function ws2_connect Lib "Ws2_32" Alias "connect" _
  121.     (ByVal As LongPtr, ByRef name As sockaddr_in, ByVal namelen As LongAs Long
  122.  
  123. 'int WSAAPI send(
  124. '  SOCKET     s,
  125. '  const char *buf,
  126. '  int        len,
  127. '  int        flags
  128. ');
  129. Private Declare Function ws2_send Lib "Ws2_32" Alias "send" _
  130.     (ByVal As LongPtr, ByVal buf As LongPtr, ByVal buflen As LongByVal flags As LongAs Long
  131.  
  132.  
  133. Private Declare Function ws2_shutdown Lib "Ws2_32" Alias "shutdown" _
  134.         (ByVal As LongByVal how As LongAs Long
  135.  
  136. Private Declare Sub WSACleanup Lib "Ws2_32" ()
  137.  
  138. Private Enum eShutdownConstants
  139.     SD_RECEIVE = 0  '#define SD_RECEIVE      0x00
  140.     SD_SEND = 1     '#define SD_SEND         0x01
  141.     SD_BOTH = 2     '#define SD_BOTH         0x02
  142. End Enum
  143.  
  144. Sub TestPortLongToBytes()
  145.     'redis is on port number 6379
  146.     Dim abytPortAsBytes() As Byte
  147.     abytPortAsBytes() = PortLongToBytes(6379)
  148.     Debug.Assert abytPortAsBytes(0) = 24
  149.     Debug.Assert abytPortAsBytes(1) = 235
  150. End Sub
  151.  
  152. Private Function PortLongToBytes(ByVal lPort As IntegerAs Byte()
  153.     ReDim abytReturn(0 To 1) As Byte
  154.     abytReturn(0) = lPort \ 256
  155.     abytReturn(1) = lPort Mod 256
  156.     PortLongToBytes = abytReturn()
  157. End Function
  158.  
  159. Private Sub TestWS2SendAndReceive()
  160.  
  161.     Dim sResponse As String
  162.     Const clJavaPort As Long = 6666
  163.     Const clRubyPort As Long = 3000
  164.     If WS2SendAndReceive(clJavaPort, "1+3" & vbCrLf, sResponse) Then
  165.         Debug.Print sResponse
  166.     End If
  167.     If WS2SendAndReceive(clRubyPort, "6*7" & vbCrLf, sResponse) Then
  168.         Debug.Print sResponse
  169.     End If
  170. End Sub
  171.  
  172.  
  173. Public Function WS2SendAndReceive(ByVal lPort As Long,
  174.             ByVal sText As StringByRef psResponse As StringAs Boolean
  175.     'https://docs.microsoft.com/en-gb/windows/desktop/api/winsock/nf-winsock-recv
  176.     If Right$(sText, 2) <> vbCrLf Then Err.Raise vbObjectError, , "Best suffix your sends with a new line (vbCrLf)"
  177.     psResponse = ""
  178.     '//----------------------
  179.     '// Declare and initialize variables.
  180.     Dim iResult As Integer : iResult = 0
  181.     Dim wsaData As udtWSADATA
  182.  
  183.     Dim ConnectSocket As LongPtr
  184.  
  185.     Dim clientService As sockaddr_in
  186.  
  187.     Dim sendBuf() As Byte
  188.     sendBuf = StrConv(sText, vbFromUnicode)
  189.  
  190.     Const recvbuflen As Long = 512
  191.     Dim recvbuf(0 To recvbuflen - 1) As Byte
  192.  
  193.     '//----------------------
  194.     '// Initialize Winsock
  195.     Dim eResult As Long 'winsockErrorCodes2
  196.     eResult = WSAStartup(&H202, wsaData)
  197.     If eResult <> 0 Then
  198.         Debug.Print "WSAStartup failed with error: " & eResult
  199.         WS2SendAndReceive = False
  200.         GoTo SingleExit
  201.     End If
  202.  
  203.  
  204.     '//----------------------
  205.     '// Create a SOCKET for connecting to server
  206.     ConnectSocket = ws2_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
  207.     If ConnectSocket = INVALID_SOCKET Then
  208.         Dim eLastError As Long 'winsockErrorCodes2
  209.         eLastError = WSAGetLastError()
  210.         Debug.Print "socket failed with error: " & eLastError
  211.         Call WSACleanup
  212.         WS2SendAndReceive = False
  213.         GoTo SingleExit
  214.     End If
  215.  
  216.  
  217.     '//----------------------
  218.     '// The sockaddr_in structure specifies the address family,
  219.     '// IP address, and port of the server to be connected to.
  220.     clientService.sin_family = AF_INET
  221.  
  222.     clientService.sin_addr(0) = 127
  223.     clientService.sin_addr(1) = 0
  224.     clientService.sin_addr(2) = 0
  225.     clientService.sin_addr(3) = 1
  226.  
  227.     Dim abytPortAsBytes() As Byte
  228.     abytPortAsBytes() = PortLongToBytes(lPort)
  229.  
  230.     clientService.sin_port(1) = 235  '* 6379
  231.     clientService.sin_port(0) = 24
  232.  
  233.     clientService.sin_port(1) = abytPortAsBytes(1)
  234.     clientService.sin_port(0) = abytPortAsBytes(0)
  235.  
  236.     '//----------------------
  237.     '// Connect to server.
  238.  
  239.     iResult = ws2_connect(ConnectSocket, clientService, LenB(clientService))
  240.     If (iResult = SOCKET_ERROR) Then
  241.  
  242.         eLastError = WSAGetLastError()
  243.  
  244.         Debug.Print "connect failed with error: " & eLastError
  245.         Call ws2_closesocket(ConnectSocket)
  246.         Call WSACleanup
  247.         WS2SendAndReceive = False
  248.         GoTo SingleExit
  249.     End If
  250.  
  251.     '//----------------------
  252.     '// Send an initial buffer
  253.     Dim sendbuflen As Long
  254.     sendbuflen = UBound(sendBuf) - LBound(sendBuf) + 1
  255.     iResult = ws2_send(ConnectSocket, VarPtr(sendBuf(0)), sendbuflen, 0)
  256.     If (iResult = SOCKET_ERROR) Then
  257.         eLastError = WSAGetLastError()
  258.         Debug.Print "send failed with error: " & eLastError
  259.  
  260.         Call ws2_closesocket(ConnectSocket)
  261.         Call WSACleanup
  262.         WS2SendAndReceive = False
  263.         GoTo SingleExit
  264.     End If
  265.  
  266.     'Debug.Print "Bytes Sent: ", iResult
  267.  
  268.     '// shutdown the connection since no more data will be sent
  269.     iResult = ws2_shutdown(ConnectSocket, SD_SEND)
  270.     If (iResult = SOCKET_ERROR) Then
  271.  
  272.         eLastError = WSAGetLastError()
  273.         Debug.Print "shutdown failed with error: " & eLastError
  274.  
  275.         Call ws2_closesocket(ConnectSocket)
  276.         Call WSACleanup
  277.         WS2SendAndReceive = False
  278.         GoTo SingleExit
  279.     End If
  280.  
  281.     ' receive only one message (TODO handle when buffer is not large enough)
  282.  
  283.     iResult = ws2_recv(ConnectSocket, VarPtr(recvbuf(0)), recvbuflen, 0)
  284.     If (iResult > 0) Then
  285.         'Debug.Print "Bytes received: ", iResult
  286.     ElseIf (iResult = 0) Then
  287.         Debug.Print "Connection closed"
  288.         WS2SendAndReceive = False
  289.         Call ws2_closesocket(ConnectSocket)
  290.         Call WSACleanup
  291.         GoTo SingleExit
  292.     Else
  293.         eLastError = WSAGetLastError()
  294.         Debug.Print "recv failed with error: " & eLastError
  295.     End If
  296.  
  297.     psResponse = Left$(StrConv(recvbuf, vbUnicode), iResult)
  298.  
  299.     'Debug.Print psResponse
  300.  
  301.     '// close the socket
  302.     iResult = ws2_closesocket(ConnectSocket)
  303.     If (iResult = SOCKET_ERROR) Then
  304.  
  305.         eLastError = WSAGetLastError()
  306.         Debug.Print "close failed with error: " & eLastError
  307.  
  308.         Call WSACleanup
  309.         WS2SendAndReceive = False
  310.         GoTo SingleExit
  311.     End If
  312.  
  313.     Call WSACleanup
  314.     WS2SendAndReceive = True
  315.  
  316. SingleExit:
  317.     Exit Function
  318. ErrHand:
  319.  
  320. End Function

Sockets - Java to Java - Sockets programming

Like most great languages Java has the ability open and read/write from/to sockets, a TCP/IP programming protocol lower than HTTP. This month I have been demonstrating Excel VBA acting as one endpoint for a Sockets connection (specifically communicating to Redis). That Excel VBA can talk to sockets means we can get VBA to talk to Java. I will write an VBA to Java article shortly but first it is better to write a Java to Java sockets application to see what is involved.

(And the more attentive amongst you will spot that I have just written a Ruby to Ruby sockets application so let's see if its true than Java is more verbose.)

The following code is a simple single-thread app (if you want multi-threaded, see the original article as we are here to demonstrate the sockets only.

JavaSocketsCalcClient.java

So this is the client code which simply reads a line of input in the form of a sum like "1+3" (actually you can do subtraction, divide and multiply), calls the server via sockets and then prints the return.

Here is the code for the file JavaSocketsCalcClient.java

//With thanks to https://www.baeldung.com/a-guide-to-java-sockets
import java.net.*;
import java.io.*;

public class JavaSocketsCalcClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        try {
            clientSocket = new Socket(ip, port);
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public String sendMessage(String msg) {
        try {
            out.println(msg);
            String resp = in.readLine();
            return resp;
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
            return "";
        }
    }

    public void stopConnection() {
        try {
            in.close();
            out.close();
            clientSocket.close();
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public static void main(String[] args) {
        try {
            GreetClient client = new GreetClient();
            client.startConnection("127.0.0.1", 6666);
            InputStreamReader reader = new InputStreamReader(System.in);
            BufferedReader in = new BufferedReader(reader);
            while (true) {
                System.out.println(client.sendMessage(in.readLine()));
            }
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }
}

So compile and run you need the following commands (and I'm surprised we do not use the .class suffix in the second to be honest).

N:\java\javaSockets>javac JavaSocketsCalcClient.java
N:\java\javaSockets>java JavaSocketsCalcClient

JavaSocketsCalcServer.java

This is the server which takes the sum string e.g. "1+3" and then parses it with a regular expression before calculating and returning the answer to the client.

//With thanks to https://www.baeldung.com/a-guide-to-java-sockets
import java.net.*;
import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class JavaSocketsCalcServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("Running on port " + port);

            String pattern = "(\\d+)\\s*(\\+|\\*|-|\\/)\\s*(\\d+)";

            // Create a Pattern object
            Pattern r = Pattern.compile(pattern);

            while (true) {

                clientSocket = serverSocket.accept();
                out = new PrintWriter(clientSocket.getOutputStream(), true);
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

                try {
                    System.out.println("waiting to read line ...");
                    String sum = in.readLine();
                    System.out.println("Received text:" + sum);
                    // Now create matcher object.
                    Matcher m = r.matcher(sum);
                    if (m.find()) {
                        Float arg0 = Float.parseFloat(m.group(1));
                        String op = m.group(2);
                        Float arg1 = Float.parseFloat(m.group(3));
                        String resp = "";
                        if (op.equals("+")) {
                            resp = String.valueOf(arg0 + arg1);
                        } else if (op.equals("-")) {
                            resp = String.valueOf(arg0 - arg1);
                        } else if (op.equals("*")) {
                            resp = String.valueOf(arg0 * arg1);
                        } else if (op.equals("/")) {
                            resp = String.valueOf(arg0 / arg1);
                        } else {
                            out.println("could not match operand");
                        }

                        out.println(sum + " = " + resp);
                    } else {
                        out.println("does not look calculable");
                    }
                } catch (Exception exc) {
                    System.out.println(exc.getMessage());
                }
            }
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public void stop() {
        try {
            in.close();
            out.close();
            clientSocket.close();
            serverSocket.close();
        } catch (Exception exc) {
            System.out.println(exc.getMessage());
        }
    }

    public static void main(String[] args) {
        GreetServer server = new GreetServer();
        server.start(6666);
    }
}

So in a separate console window, do the following

N:\java\javaSockets>javac JavaSocketsCalcServer.java
N:\java\javaSockets>java JavaSocketsCalcServer

Here is a screenshot of the two consoles running and communicating

Keen observers of the code will realise they both run in endless loops. Never mind about that, I simply wanted to get the servers running and demonstrable.

What's Next?

So what's next is some code which shows VBA calling this Java server and also the Ruby server from the previous article to illustrate VBA can call Java and Ruby despite them not having a COM APIs.

Thursday, 2 August 2018

Python - Java - Nu Html Checker - Running an HTML validator on old help pages

So in previous post I showed how to use HTMLTidy to restructure old HTML help pages in that case decompiled from a help file (*.chm) but could apply to any old HTML files. To raise compliance to HTML5, it is still necessary to further triage them. In its output messages, HTMLTidy recommends validating at http://validator.w3.org/nu/ but in this post we show how one can run this logic locally by downloading the java jar that drives that web site.

The Nu Html Checker

So HTML Tidy recommends the useful web site Nu Html Checker, https://validator.w3.org/nu/#textarea but before you feel tempted to write code to script against this page be advised you can run your own copy of the Nu Html Checker from a command line so long as you have Java installed.

Install Java

Do please install Java before attempting the code below

Install Nu Html Checker

Instructions as to how to get your own copy of the tool are here. So I navigated to Nu Html Checker version 18.7.23 and downloaded vnu.jar_18.7.23.zip . When the download completed, I unzipped it and extracted contained files to a subdirectory in my Downloads folder. For later use, I defined an environment variable %vnu% to point to vnu.jar's parent folder, %userprofile%\Downloads\vnu.jar_18.7.23\dist . A better long run place to install would be somewhere in Program Files.

Running Nu Html Checker from command line

With the environment variable %vnu% defined I can test the install is working (both java and the downloaded jar file) with ...

C:\>java -jar %vnu%\vnu.jar --version
18.7.23

You can see the Nu Html Checker version number is returned, so all is installed correctly.

Running Nu Html Checker from command line on a single file

Installation confirmed, we can confidently advance to running the tool on an HTML file, I have some files resulting from a previous post. So I will try this file

C:\>java -jar %vnu%\vnu.jar --no-langdetect --format xml %Temp%\HelpFileDecompiler\VBLR6\vblr6.hhc.tidied.html
<?xml version='1.0' encoding='utf-8'?>
<messages xmlns="http://n.validator.nu/messages/">
<error url="file:/C:/Users/Simon/AppData/Local/Temp/HelpFileDecompiler/VBLR6/vblr6.hhc.tidied.html" last-line="8" last-column="15" first-column="8">
<message>Element <code xmlns="http://www.w3.org/1999/xhtml">title</code> must not be empty.</message>
<extract>-&gt;
&lt;title&gt;<m>&lt;/title&gt;</m>
&lt;/hea</extract>
</error>

</messages>

C:\>

So we get a report. In this case one message only complaining about an empty title element; the message carries text file co-ordinates (line, column) so we can locate easily. Some of the message is itself entitized HTML and so reads a little cryptically but the other output formats are not much better.

Running Nu Html Checker from command line on a directory

Running on a whole directory created a huge massive file. I'd prefer a report file per HTML file. Fortunately we can write some Python code to do this.

Python Script to walk a folder and run Nu Html Checker on each file

If you have been reading my Python posts then the next script follows a familiar pattern. The script has a COM callable class so Excel VBA can call into it but it also stands alone and is callable by running Python from the command line. This is an Excel blog and I feel obliged to tie non VBA code back to VBA. In fact, there are two classes, I am working on a series of posts and would like to reuse the naming logic so that explains the HTMLTidiedChmFileNamer class.

The ValidatorReporter class runs the Nu Html Checker validation checker. It walks a folder as found in previous scripts. It shells a process using subprocess as in previous scripts. One thing that is new here is that we are shelling to java. Another things that is new here is that we are capturing the stderr by specifying PIPE in subprocess.run() arguments; this allows us to read the stderr stream and then we write it to a file.

import os
import subprocess
from subprocess import PIPE
import codecs


class HTMLTidiedChmFileNamer(object):
    _reg_clsid_ = "{8807D2B9-C83F-4AEB-A71D-15DBE8EFED9A}"
    _reg_progid_ = 'PythonInVBA.HTMLTidiedChmFileNamer'
    _public_methods_ = ['TidiedFilenameWin32Dict']

    def TidiedFilename(self, subdir, file):
        file2 = file.lower()
        tidiedFile = ""
        errorfile = ""
        validationErrorsFile = ""

        if ".tidied." not in file:
            if file2.endswith((".hhc", ".hhk")):
                tidiedFile = subdir + os.sep + file + ".tidied.html"
                errorfile = subdir + os.sep + file + ".tidied.errors.txt"
                validationErrorsFile = (subdir + os.sep + file +
                                        ".tidied.validationErrors.txt")

            if file2.endswith((".htm", ".html")):
                tidiedFile = (subdir + os.sep +
                              file.split('.')[0] + ".tidied.html")
                errorfile = (subdir + os.sep +
                             file.split('.')[0] + ".tidied.errors.txt")
                validationErrorsFile = (subdir + os.sep +
                                        file.split('.')[0] +
                                        ".tidied.validationErrors.txt")
        return (tidiedFile, errorfile, validationErrorsFile)


class ValidatorReporter(object):
    _reg_clsid_ = "{321F338F-75AE-460B-85A2-5C553A39CDE1}"
    _reg_progid_ = 'PythonInVBA.ValidatorReporter'
    _public_methods_ = ['ValidateBatch']

    def ValidateBatch(self, rootDir):

        if "vnu" not in os.environ:
            raise Exception(
                "vnu environment variable not defined, "
                "please define as vnu jar's parent folder")

        sVNUExe = os.path.join(os.environ["vnu"], "vnu.jar")
        FileNamer = HTMLTidiedChmFileNamer()

        for subdir, dirs, files in os.walk(rootDir):
            for file in files:
                tidiedFile, errorfile, validationErrorsFile = 
                        FileNamer.TidiedFilename(subdir, file)
                if not tidiedFile == "":
                    # https://github.com/validator/validator#user-content-usage
                    args = ['java', '-jar', sVNUExe, '--no-langdetect',
                            '--format', 'xml', tidiedFile]
                    proc = subprocess.run(args, stderr=PIPE)

                    file = codecs.open(validationErrorsFile, "w", "utf-8")
                    file.write(proc.stderr.decode("utf-8"))
                    file.close()

if __name__ == '__main__':
    print ("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(ValidatorReporter)
    win32com.server.register.UseCommandLine(HTMLTidiedChmFileNamer)
    
    rootdir = os.path.join(os.environ["tmp"], 'HelpFileDecompiler', "vblr6")
    test = ValidatorReporter()
    test.ValidateBatch(rootdir)

The portion of code that registers the COM classes require administrator rights. You can comment them out and run the script from command line instead in a purely Pythonic way.

The code assumes you have a folder with HTML files in it. For me I have given the code a folder of HTML files extracted from a decompiled *.chm file and the code takes a good while.

Client VBA Code

To prove we can call this Python script from VBA here is the client code

Sub TestValidatorReporter()
    
    Dim objValidatorReporter As Object
    Set objValidatorReporter = VBA.CreateObject("PythonInVBA.ValidatorReporter")
    
    objValidatorReporter.ValidateBatch Environ$("tmp") & "\HelpFileDecompiler\VBLR6\"

End Sub

Final Thoughts

For me, the resultant output is huge and will take time to comb through but it looks like I'll need to load HTML files into Xml parsers and rearrange attributes etc. More Python code to come in this series. So look out for that.

Tuesday, 26 September 2017

Excel on the Server? No thanks, Xml ADO recordsets please

I have encountered a variety of what I would call "Excel on the server" technologies and these include Microsoft SharePoint Server but also there is an Apache (and thus open source) Java Apache-POI, I chanced upon the latter whilst looking at StackOverflow bounties. Mulling the use case of generating excel workbooks on a server I think that the majority use case is the creation of reports, and the best way to do this is pivot tables and charts based on those pivot table. But is the creation of pivot tables in an Excel workbook on a server a smart thing to do? If you look at some sample Apachi-POI code it would appear a bit clunky.

In this older post I show worksheet cell contents converted to Xml and then to an ActiveX Data Objects (hereafter ADO) recordset. Use of ADO recordsets as a means to marshalling data between a client desktop and a computer room server should not be underestimated. Indeed, in the era of Visual Basic 6 the N-tier architecture was Windows DNA and all these distributed architectures require some state container/vessel to marshal data from one tier to another. For Windows DNA an ADO recordset that the state marshalling container/vessel.

So I would recommend web services emitting a Xml version of an ADO recordset to an Excel workbook. The magic line of code that eliminates a ton of scripting is the CopyFromRecordset method, it is the penultimate line in the following VBA example. You'll need the Xml to be saved into a file (I have chosen c:\temp\xl_persists_2.xml)

<xml xmlns:x="urn:schemas-microsoft-com:office:excel" 
    xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" 
    xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" 
    xmlns:rs="urn:schemas-microsoft-com:rowset" 
    xmlns:z="#RowsetSchema">
<x:PivotCache>
<x:CacheIndex>1</x:CacheIndex>
<s:Schema id="RowsetSchema">
<s:ElementType name="row" content="eltOnly">
<s:attribute type="Col1"/>
<s:attribute type="Col2"/>
<s:attribute type="Col3"/>
<s:extends type="rs:rowbase"/>
</s:ElementType>
<s:AttributeType name="Col1" rs:name="FirstName">
<s:datatype dt:maxLength="255"/>
</s:AttributeType>
<s:AttributeType name="Col2" rs:name="FamilyName">
<s:datatype dt:maxLength="255"/>
</s:AttributeType>
<s:AttributeType name="Col3" rs:name="Role">
<s:datatype dt:maxLength="255"/>
</s:AttributeType>
</s:Schema>
<rs:data>
<z:row Col1="John" Col2="Snow" Col3="President"/>
<z:row Col1="Ygritte" Col2="Wild" Col3="Vice-President"/>
</rs:data>
</x:PivotCache>
</xml>

For the VBA you'll need Tools->References to Microsoft ActiveX Data Object 6.1 Library (or similar) and Microsoft Xml, v6.0 (or similar)

Function RecordsetAsXml() As String
    '* in this example I'm loading from a file but it can be a webservice.
    
    RecordsetAsXml = VBA.CreateObject("Scripting.FileSystemObject").OpenTextFile("c:\temp\xl_persist_2.xml").ReadAll
End Function

Sub LoadXmlRecordset()

    'Tools->References:Microsoft ActiveX Data Object 6.1 Library
    Dim rs As ADODB.Recordset
    
    'Tools->References:Microsoft Xml, v6.0
    Dim domRecordsetAsXml As MSXML2.DOMDocument60
    Set domRecordsetAsXml = New MSXML2.DOMDocument60
    domRecordsetAsXml.LoadXML RecordsetAsXml
    Debug.Assert domRecordsetAsXml.parseError.ErrorCode = 0

    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    rs.Open domRecordsetAsXml
    
    '* placed a little under the original data for comparison
    Dim rngOrigin As Excel.Range
    Set rngOrigin = ThisWorkbook.Worksheets.Item(1).Cells(6, 1)
    
    Dim lFieldLoop As Long
    For lFieldLoop = 0 To rs.Fields.Count - 1
        rngOrigin.Offset(0, lFieldLoop).Value = rs.Fields(lFieldLoop).Name
    Next lFieldLoop
    
    rngOrigin.Offset(1).CopyFromRecordset rs

End Sub



From this point it is very easy to generate a pivot table and charts from the table of data zapped into the worksheet by CopyFromRecordSet. So, I prefer Xml ADO recordsets to Sharepoint or Apache POI generated workbooks.