Scott Hanselman

WatirMaker written again in Ruby

July 7, '06 Comments [1] Posted in ASP.NET | Ruby | Watir
Sponsored By

WatirmakerrubyI always thought WatirMaker was a pretty good idea. It was meant not as a recorder, per se, but rather as a "faster typer."

I use it to jump start spikes like my recent Vonage script. If I wrote Watir often enough I'd just use the Ruby interactive shell.

(By the way, if you  have 15 minutes - maybe it's lunch - visit here: http://tryruby.hobix.com/ and try Ruby out, guilt- and install-free, in your browser.)

After I did WatirMaker in C#, Michael Kelly and John Hann wrote it again in native Ruby with the tiniest bit of help from me early on. It's rockin' sweet IMHO.

You can run it by simply running "ruby watirmaker.rb" from the command-line or by redirecting to a file "ruby watirmaker.rb > myscript.rb."

John emailed me and said that he and Michael are going to look for a permanent home for this, but until then it's here.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Serializing Objects as JavaScript using Atlas, JSON.NET and AjaxPro

July 5, '06 Comments [25] Posted in ASP.NET | Ruby | Javascript | TechEd | Speaking | Web Services
Sponsored By

Ajax is shiny. In our talk at TechEd, Patrick and I mentioned that our next plan was a dynamic endpoint for our financial services that spoke JSON to complement our "Dirty SOAP" endpoint. This would make auto-complete dropdowns and sortable grids REALLY easy when interfacing with our SDK that already supports a large message set for banking-type things like GetPayees, GetAccountHistory.

The first step to make this happen will be JSON serialization round-tripping. For example, I'd like to take this object (instance)...

public class Person

{

    public string firstName = "Scott";

    public string lastName = "Hanselman";

    public DateTime birthDay = new DateTime(1970, 1, 15, 1, 1, 0);

    public decimal moneyInPocket = 4.5M;

}

...and serialize it to JSON thusly:

{"firstName":"Scott", "lastName":"Hanselman", "birthDay": new Date(1213260000), "moneyInPocket":4.5}

I was already planning to create a JavaScript serializer as Corillian already has fixed-length, delimited, name-value pair and other serializers for any object.

I took a look at JSON.NET thinking it'd be a nice, lightweight serializer, and while it's cool on initial first glance, this release didn't pass the "fall into the pit of success" test for an object serializer.

UPDATE: Json.NET has been updated and now works as expected and includes helper methods to make the job simpler...new code below.

    1 using System;

    2 using System.IO;

    3 using System.Collections.Generic;

    4 using System.Text;

    5 

    6 namespace ConsoleApplication1

    7 {

    8     public class Person

    9     {

   10         public string firstName = "Scott";

   11         public string lastName = "Hanselman";

   12         public DateTime birthDay = new DateTime(1970, 1, 15, 1, 1, 0);

   13         public decimal moneyInPocket = 4.5M;

   14     }

   15 

   16     class Program

   17     {

   18         static void Main(string[] args)

   19         {

   20             Person p = new Person();

   21             string output =  Newtonsoft.Json.JavaScriptConvert.SerializeObject(p);

   22 

   23             output = output.Replace("Scott", "Fred");

   24             output = output.Replace("Hanselman", "Jones");

   25 

   26             Person anotherP = Newtonsoft.Json.JavaScriptConvert.DeserializeObject(output, typeof(Person)) as Person;

   27             Console.WriteLine(anotherP.firstName + " " + anotherP.lastName);

   28         }

   29     }

   30 }

   31 

Then I tried Ajax.NET 6.7.2.1 from Michael Schwarz...

    1 using System;

    2 using System.IO;

    3 using System.Collections.Generic;

    4 using System.Text;

    5 using Microsoft.Web.Script.Serialization;

    6 

    7 namespace ConsoleApplication1

    8 {

    9     public class Person

   10     {

   11         public string firstName = "Scott";

   12         public string lastName = "Hanselman";

   13         public DateTime birthDay = new DateTime(1970, 1, 15, 1, 1, 0);

   14         public decimal moneyInPocket = 4.5M;

   15     }

   16 

   17     class Program

   18     {

   19         static void Main(string[] args)

   20         {

   21             Person p = new Person();

   22             string output = AjaxPro.JavaScriptSerializer.Serialize(p);

   23 

   24             output = output.Replace("Scott", "Fred");

   25             output = output.Replace("Hanselman", "Jones");

   26 

   27             Person anotherP = AjaxPro.JavaScriptDeserializer.DeserializeFromJson(output, typeof(Person)) as Person;

   28             Console.WriteLine(anotherP.firstName + " " + anotherP.lastName);

   29         }

   30     }

   31 }

   32 

...but got this exception on the deserialization. When reflectoring through the source, this implies that null was passed to DateTimeConverter.Deserialize. I think an ArgumentNullException would have been clearer.

System.NotSupportedException was unhandled
  Message="Specified method is not supported."
  Source="AjaxPro.2"
  StackTrace:
       at AjaxPro.DateTimeConverter.Deserialize(IJavaScriptObject o, Type t)
       at AjaxPro.JavaScriptDeserializer.Deserialize(IJavaScriptObject o, Type type)
       at AjaxPro.JavaScriptDeserializer.DeserializeCustomObject(JavaScriptObject o, Type type)
       at AjaxPro.JavaScriptDeserializer.Deserialize(IJavaScriptObject o, Type type)
       at AjaxPro.JavaScriptDeserializer.DeserializeFromJson(String json, Type type)
       at ConsoleApplication1.Program.Main(String[] args) in C:\Documents and Settings\Scott\Desktop\ConsoleApplication1\ConsoleApplication1\Program.cs:line 29

However, I was able to get it to round-trip when I removed the DateTime. Not sure what's up with that.

Also interesting, Ajax.NET (AjaxPro) saved the Type/Assembly Qualified Name in the resulting JSON. I can see why they'd want to do that, but one of the nice things about JavaScript and JSON in general is the cleanliness and flexibility of the wire format. This could complicate things if I've got different CLR types on the server consuming the same serialized JSON from the client. It also serializes the DateTime in a different way than I'm used to.

{"__type":"ConsoleApplication1.Person, ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "firstName":"Scott", "lastName":"Hanselman", "birthDay": new Date(Date.UTC(1970,0,15,9,1,0,0)), "moneyInPocket":4.5}

Moving to Microsoft's Atlas, similar and slightly simpler code works just fine like this:

    1 using System;

    2 using System.IO;

    3 using System.Collections.Generic;

    4 using System.Text;

    5 using Microsoft.Web.Script.Serialization;

    6 

    7 namespace ConsoleApplication1

    8 {

    9     public class Person

   10     {

   11         public string firstName = "Scott";

   12         public string lastName = "Hanselman";

   13         public DateTime birthDay = new DateTime(1970, 1, 15, 1, 1, 0);

   14         public decimal moneyInPocket = 4.5M;

   15     }

   16 

   17     class Program

   18     {

   19         static void Main(string[] args)

   20         {

   21             Person p = new Person();

   22             string output = JavaScriptObjectSerializer.Serialize(p);

   23 

   24             output = output.Replace("Scott", "Fred");

   25             output = output.Replace("Hanselman", "Jones");

   26 

   27             Person anotherP = JavaScriptObjectDeserializer.Deserialize(output, typeof(Person)) as Person;

   28             Console.WriteLine(anotherP.firstName + " " + anotherP.lastName);

   29         }

   30     }

   31 }

   32 

...giving the expected output of

{"firstName":"Scott", "lastName":"Hanselman", "birthDay": new Date(1213260000), "moneyInPocket":4.5}

Between these three JSON serializers and this simple test, only ATLAS "just worked." (Assuming my 'simple test' isn't flawed.) For now I'll use these Atlas assemblies for my JSON serialization needs, but it'd be nice if I could back-port the chunky parts of one of the other libraries to .NET 1.1 my for projects that can't use 2.0.

UPDATE: Json.NET and Atlas appear to work the same with this simple test.

Of course, the really sad thing is that John Lam or my boss will step in here any minute and remind us that in Ruby on Rails you can just say object.to_json and get JSON strings. Phooey!

As a totally unrelated aside, and for the purposes of starting a discussion - in the JSON.NET source code the author James Newton-King appears to have decompiled the source for System.Web.UI.Util.QuoteJScriptString from ASP.NET and included the decompiled source (with a few modifications) directly in JSON.NET's JavaScriptUtils.cs which is then licensed under the Creative Commons Attribution 2.5 License. 

UPDATE: James Newton King updated the JSON Framework and removed the CC license from the JavaScriptUtils.cs file as it wasn't his intent to release Microsoft code like that. He also included an explanatory comment. Seems like a reasonable solution to me.

The question is: When a useful function exists in the .NET Framework, but is marked internal/private, for whatever reason. Is it better to:

A. decompile it and include it in your code anyway (hopefully with an explanatory comment).
B. write your own from scratch.
C. use the internal/private/whatever method via Reflection.

Discuss.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

An Xml Tidy in PowerShell or Formatting Xml with Indenting with PowerShell

July 3, '06 Comments [4] Posted in PowerShell | XML
Sponsored By


I like my XML pretty. There's no format-xml cmdlet or tidy-xml in PowerShell, so here's my first try:

#Name me tidy-xml.ps1
# - this crap written by Scott Hanselman
[System.Reflection.Assembly]::LoadWithPartialName("System.Xml") > $null
$PRIVATE:tempString = ""
if ($args[0].GetType().Name -eq "XmlDocument")
{
 $PRIVATE:tempString = $args[0].get_outerXml()
}
if ($args[0].GetType().Name -eq "String")
{
 $PRIVATE:tempString = $args[0]
}
$r = new-object System.Xml.XmlTextReader(new-object System.IO.StringReader($PRIVATE:tempString))
$sw = new-object System.IO.StringWriter
$w = new-object System.Xml.XmlTextWriter($sw)
$w.Formatting = [System.Xml.Formatting]::Indented
do { $w.WriteNode($r, $false) } while ($r.Read())
$w.Close()
$r.Close()
$sw.ToString()

Sometimes XML is thought of as strings and sometimes as [xml] in PowerShell. This script will take either a string or [xml] but will always return a string. (e.g. It's on you to do the final [xml] cast because if you did, the tidying is moot). For example:

PS> $a = "<foo><bar>asdasd</bar></foo>"
PS> ./tidy-xml $a
<foo>
  <bar>asdasd</bar>
</foo>
PS> $b = [xml]"<foo><bar>asdasd</bar></foo>"
PS> ./tidy-xml $b
<foo>
  <bar>asdasd</bar>
</foo>

I wanted to make it so I could do these scenarios. Thoughts? Remember that I need to normalize to a string for the StringReader constructor.

#couldn't because it returned an Object[] of strings and it got sloppy fast
get-content foo.xml | tidy-xml

#couldn't because it (oddly) returned an ArrayList of strings and it got sloppy fast
get-content foo.xml -ov c
tidy-xml $c

Enjoy (or improve!)

UPDATE: Here's a better version that includes a number of best-practices changes as well as the support for taking IN objects from the pipeline (like I wanted originally):

#The following cases work
#
#PS>$a
#<foo><bar>this is A</bar></foo
#PS>$b.get_OuterXml()
#<foo><bar>this is B</bar></foo
#PS>Get-Content foo.xml
#<foo>
#   <bar>this is C</bar>
#</foo>
#
#Now try the following.
#PS>sal ti tidy-xml
#PS>$a | ti
#PS>$b | ti
#PS>$c | ti
#PS>ti $a
#PS>ti $b
#PS>ti $c
#PS>$a, $b | ti
#PS>$a, $c | ti
#PS>$c, $b | ti
#PS>$a, $b, $c | ti
#
#What doesn't work here is when you pass a multiple parameter input as follows:
#tidy-xml $a, $b # doesn't work
#
#Uhm, i think i would have to change my logic "completely" to actually get that to work...
#(after refactoring "process" block...)
#
#Name me tidy-xml.ps1
# - some of this crap written by Scott Hanselman
function Tidy-Xml {
    begin {
        $private:str = ""
       
        # recursively concatenate strings from passed-in arrays of schmutz
        # not sure how to improve this...
        function ConcatString ([object[]] $szArray) {
            # return string
            $private:rStr = ""

            # Recursively call itself, if a string is also of array or a collection type
            foreach ($private:sz in $szArray) {
                if (($private:sz.GetType().IsArray) -or `
                    ($private:sz -is [System.Collections.IList])) {
                    $private:rStr += ConcatString($private:sz)
                }
                elseif ($private:sz -is [xml]) {
                    $private:rStr += $private:sz.Get_OuterXml()
                }
                else {
                    $private:rStr += $private:sz
                }
            }
            return $private:rStr;
        }
       
        # Original "Tidy-Xml" portion
        function FormatXmlString ($arg) {
            # ignore parse errors
            trap { continue; }
           
            # out-null hides output of the assembly load
            [System.Reflection.Assembly]::LoadWithPartialName("System.Xml") | out-null

            $PRIVATE:tempString = ""
            if ($arg -is [xml]){
                $PRIVATE:tempString = $arg.get_outerXml()
            }
            if ($arg -is [string]){
                $PRIVATE:tempString = $arg
            }

            # the ` tick mark is a line-continuation char
            $r = new-object System.Xml.XmlTextReader(`
                new-object System.IO.StringReader($PRIVATE:tempString))
            $sw = new-object System.IO.StringWriter
            $w = new-object System.Xml.XmlTextWriter($sw)
            $w.Formatting = [System.Xml.Formatting]::Indented

            do { $w.WriteNode($r, $false) } while ($r.Read())

            $w.Close()
            $r.Close()
            $sw.ToString()
        }
    }
   
    process {
        # For non-xml strings or types, they will be buffered and will be
        # taken care of in "end" block
        
        # this checks for objects that have been "pipe'd" in.
        if ($_) {
            # check if whatever we have appended is a valid XML or not
            $private:xmlStr = ($private:str + $_) -as [xml]
           
            if ($private:xmlStr -ne $null) {
                FormatXmlString([xml]$private:xmlStr)
                # clear the string not to be handled in "end" block
                $private:str = $null
            } else {
                if ($_ -is [string]) {
                    $private:str += $_
                } elseif ($_ -is [xml]) {
                    FormatXmlString($_)
                }
                # for an array or a collection type,
                elseif ($_.Count) {
                    # iterate each item in the collection and append
                    foreach ($i in $_) {
                        $private:line += $i
                    }
                    $private:str += $private:line
                }
            }
        }
    }

    end {
        if ([string]::IsNullOrEmpty($private:str)) {
            $private:szXml = $(ConcatString($args)) -as [xml]
            if (! [string]::IsNullOrEmpty($private:szXml)) {
                FormatXmlString([xml]$private:szXml)
            }
        } else {
            FormatXmlString([xml]$private:str)
        }
    }
}

Thanks to MonadBlog for the Updates! There's definitely some room for refactoring of the begin/end/process, but it's more funcitonal this way.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Querying Virtual Server 2005 via VM with PowerShell

July 3, '06 Comments [4] Posted in PowerShell
Sponsored By

One of the guys in IT manages a lot of Virtual Server instances, like dozens, adding up into many dozens of Virtual Machines all supporting our many devs. He wanted to get some status information with PowerShell. Here's what I came up with.

We used WMI Explorer to check out the WMI namespace installed by Virtual Server (root/vm/virtualserver).

Given a CSV file like this full of (at least) Virtual Server ComputerName

computername,owner,whatever
MSVS1,fred,somedata
MSVS2,joe,somedata
MSVS3,luigi,somedata

We did this:

import-csv servers.csv | foreach-object
{  $_.computername   } |
foreach-object
{ Get-WmiObject -computername $_ -namespace "root/vm/virtualserver" -class VirtualMachine } |
select 
@{Expression={"__SERVER"}; Name="Server"},
Name, CpuUtilization,
select @{Expression={"Uptime/60"}; Name="Uptime(min)"},
PhysicalMemoryAllocated,
DiskSpaceUsed |
format-table -groupby Server -property name,CpuUtilization,Uptime,PhysicalMemoryAllocated,DiskSpaceUsed

Which gives us more or less this:

Vsserver1

Which can also reformat, make smaller and run in a loop to get a "top" equivalent for all our VMs. We can catch machines that are running out of space, working too hard, and do capacity planning.

Note, I originally wanted to do this:

import-csv servers.csv | Get-WmiObject -namespace "root/vm/virtualserver" -class VirtualMachine

and have "computername" automatically bound to because the name was the same in the CSV file and the powershell parameter name. This does work in this instance...make a CSV file like this named pids.csv:

ID
1
2
3
4
5

and execute this PowerShell pipeline

import-csv pids.csv | get-process

and you'll get

Get-Process : No process with process ID 1 was found.
At line:1 char:33
+ import-csv pids.csv | get-process <<<<
Get-Process : No process with process ID 2 was found.
At line:1 char:33
+ import-csv pids.csv | get-process <<<<
Get-Process : No process with process ID 3 was found.
At line:1 char:33
+ import-csv pids.csv | get-process <<<<

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
   2399       0        0         32     2   507.75      4 System
Get-Process : No process with process ID 5 was found.
At line:1 char:33
+ import-csv pids.csv | get-process <<<<

See how it called get-process for each ID and it automatically bound the ID column of the table coming from the CSV to the ID property? I wanted to do the same with with computername, but it didn't work.

Get-WmiObject : The input object cannot be bound to any parameters for the comm
and either because the command does not take pipeline input or the input and it
s properties do not match any of the parameters that take pipeline input.
At line:1 char:21

I got this error which I assume means that Get-WMIObject doesn't take pipeline input in the build of PowerShell I have (RC1). I hope this is queued to get fixed ASAP or I'm just missing something.

UPDATE: A "help get-wmiobject" (duh, RTFM) confirms that -computername doesn't take pipeline input.

    -ComputerName <System.String[]>
        Declares on which computer(s) the WMI object may be found

        Parameter required?           false
        Parameter position?           named
        Parameter type                System.String[]
        Default value                 localhost
        Accept multiple values?       true
        Accepts pipeline input?       false
        Accepts wildcard characters?  false

Bummer.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Searching Google Desktop from PowerShell

July 2, '06 Comments [1] Posted in PowerShell | Javascript | XML
Sponsored By

I just noticed that Sean McLeod created a nice cmdlet for PowerShell that lets you query Windows Desktop Search from the PowerShell Command Line. I used to use Windows Desktop Search but got tired of it hanging and switched to, and settled on, for now, Google Desktop Search and I've been very happy with it.

After reading this I said, I'd like to created a cmdlet for Google Desktop Search, but who has the time? (Maybe I'll do it later)

I figured I could do something quick and dirty though. I took a look at the Google Desktop API Developer's Guide and saw some JavaScript samples for their psycho and obscure COM API. (Google does nearly everything in C++. Even their .NET sample code smells like C++.)

I did this at the PowerShell command line, as a client of the Google Desktop Search has to "register" itself, and I got stuck:

PS>$registrar = new-object -com "GoogleDesktop.Registrar"
PS>$regArray = "Title","Searching GDS from PowerShell","Descripton","For Fun","Icon","My
Icon@1"
PS>$regId = [System.Guid]::NewGuid().ToString("B").ToUpper()
PS>$registrar.StartComponentRegistration($regId,$regArray)
Exception calling "StartComponentRegistration" with "2" argument(s): "The component description must contain a SAFEARRAY of six or eight VARIANTs" At line:1 char:38 + $registrar.StartComponentRegistration( <<<< $regId,$regArray)

...and they lost me at SAFEARRAY. I don't know why this little .NET array didn't get marshalled correctly. Probably because it's marshalled as a SAFEARRAY of BSTRs instead. Anyway, too hard, patience waning.

But, hey, GDS runs a local web server, right? So I should be able to query it locally. The docs say:

The search query URL, including your security token, is stored in the registry at:
HKEY_CURRENT_USER\Software\Google\Google Desktop\API\search_url
To use the example above, the stored query URL would be something like:
http://127.0.0.1:4664/search&s=1ftR7c_hVZKYvuYS-RWnFHk91Z0?q=

So from PowerShell:

PS>(Get-Item "HKCU:\Software\Google\Google Desktop\api").GetValue("search_url")
http://127.0.0.1:4664/search&s=1ftR7c_hVZKYvuYS-RWnFHk91Z0?q=

Cool. Then I can add the query after the "q=" and "&format=xml" to get something that PowerShell can sink its teeth into. I'll need to UrlEncode the query.

UPDATE: Note the num= and flags= at the end of the queryString. That indicates that we are only interested in files and we'll take as many as 1000.

PS>$query = "PowerShell"
PS>$searchUrl = (Get-Item "HKCU:\Software\Google\Google Desktop\api").GetValue("search_url")
PS>[System.Reflection.Assembly]::LoadWithPartialName("System.Web") > $null
PS>$newQuery = $searchUrl + [System.Web.HttpUtility]::UrlEncode($query) + "&format=xml&flags=576&num=1000"
PS>$webclient = new-object System.Net.WebClient
PS>$resultsXml = [xml]($webclient.DownloadString($newQuery))
PS>$resultsXml.results.result | select title, snippet, category

title                      snippet                    category
-----                      -------                    --------
C:\Temp\fusionlogs\Defa... NET\Framework\v2.0.5072... file
Windows <b>PowerShell</... Windows <b>PowerShell</... web
del.icio.us/shanselman     del.icio.us/shanselman ... web
C:\Temp\fusionlogs\Defa... NET\Framework\v2.0.5072... file
Windows Desktop Search ... Windows Desktop Search ... web
User Profile: Greg Borota  User Profile: Greg Boro... web
del.icio.us/shanselman     del.icio.us/shanselman ... web
Microsoft Survey            Microsoft Survey Th... web
Discussions in Windows ... Discussions in Windows ... web
Search Results: <b>powe... Search Results: <b>powe... web

Cool. So now I want just the files, and I want them to be actual FileInfo objects.

PS> $resultsXml.results.result | where { $_.category -eq "file"} | foreach-object { get-item $_.url }

    Directory: Microsoft.PowerShell.Core\FileSystem:: C:\Temp\fusionlogs\Default\powershell.exe

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---          7/2/2006   3:08 AM       1625 System.Web.HTM
-a---          7/2/2006   2:36 AM       1695 System.Windows.Forms.HTM

...etc...

Now I'll save the whole file in my path in a file called "search-googledesktop.ps1" and change the first line to "$query = $args" to take command line arguments.

Since the objects that are coming out of this script are real FileInfo objects retrieved with the call to get-item in the final line, I can use my script in a larger pipeline like this:

.\search-googledesktop.ps1 "powershell" | get-content

or

.\search-googledesktop.ps1 "powershell" | get-location

or delete any file that has my personal information like a social security number in it.

.\search-googledesktop.ps1 "123" | remove-item -whatif

Seriously, once you have an indexer on your system, search for your Social Security Number. You'd be amazed at the old Excel sheets and crap that number gets into.

Anyway, cool. Sure beats their crap COM API.

File Attachment: search-googledesktop.ps1 (474 bytes)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.