Skip to content. | Skip to navigation

Personal tools

Navigation

You are here: Home / weblog

Dominic Cronin's weblog

Showing blog entries tagged as: Powershell

Straightforward Powershell scripting with the Tridion core service

Posted by Dominic Cronin at Apr 04, 2013 11:30 PM |

Almost exactly a year ago, I blogged about Getting to grips with the Tridion core service in Powershell. The core service had been around for a while even then, and the point was to actually start using it for some of the scripting tasks I had habitually done via the TOM. In many ways the TOM was much more script-friendly. Of course, that might have had something to do with the fact that it was created expressly for use from scripting languages. The Tridion core service API wasn't. I don't know exactly what they had in mind, but I'd imagine the thinking was that most mainstream users would use C#. Yeah, sure - any compliant .NET language would do, but F#? Nah!

But a year further on, and where are all those scripts I was going to write? I have to say, the comfort zone for scripting is quite different than for writing "proper" programmes. There's huge usefulness in being able to hack out something quickly, and very much a sense that stuff will be intermingled ina-code-is-data-stylee. So when I started actually trying to use the core service for scripting tasks, it sucked pretty hard. There were two main areas of difficulty:

  1. Getting the core service wired up in the first place
  2. Powershell doesn't natively have the equivalent of C#'s using directive to allow you to avoid typing the full namespace of your type.

 

I covered the first point last year. Suffice it to say that currently, I'm still using Peter Kjaer's Tridion powershell module, although at the moment I'm running a local copy, modified to cope with the Tridion 2013 client, and also to allow me to specify which protocol I want to use. (Obviously I don't want to have a permanent fork, so with a bit of luck, Peter will be able to integrate some of this work into the next release of the module.) On a related subject, my experience has been that working with the core service client has some fundamental differences with using the TOM. You could keep a TDSE lying around for minutes at a time, and it would still be usable, even after a method call had failed. The core service, even when you're on the same server, is most definitely a web service. Failed calls tend to leave your connection in a "faulted" state (i.e. unusable), and the timeouts are generally shorter. Once you are aware of this, you can adjust your coding style accordingly, but it adds somewhat to the ritual.

The namespace issue is on the face of it more trivial. OK - so it's a PITA to have to type something like:

$folder = new-object Tridion.ContentManager.CoreService.Client.FolderData

when all you wanted was a folder. You could argue: "well it works, doesn't it? Get over it!". However, I found all this extra verbiage too much of a distraction, not only when reading and editing longer scripts, but also when "knocking off a quick one". After all, what's the point of having a great scripting environment if your one-liners aren't?

So what to do? Well I scoured the Internet, and discovered that Powershell has something called a Type Accelerator. You've seen these often enough, as there are several available by default. For example, you can (and should) type "[string]" when what you really mean is "[System.String]". Unfortunately, creating type accelerators isn't completely straightforward, but No Worries, the Powershell community is vibrant and there are implementations available that take care of it for you. (OK, at the time of writing I know of one that works, but that's enough, eh? My first Googling had taken me to the Type Accelerators module (PSTX) at codeplex. At first this seemed to be useful, but as soon as I moved to Tridion 2013, support for Powershell 3 became a hard requirement. This project is not actively maintained, and it doesn't work in Powershell 3. As I said, it's not straightforward to wire up type accelerators, and the code uses an undocumented API, which changed. Not Microsoft's fault.)

At this point, I went to the Powershell IRC channel (#powershell on freenode) and asked there if anyone knew about fixes or updates. I was steered in the direction of Jaykul's reflection module, available on Poshcode. (Make sure you get the latest version, and beware of the script getting truncated.) Installing modules is a fairly straightforward task: often as simple as dropping the files into a suitably named directory in your WindowsPowerShell modules directory (sometimes you need to "unblock" them) . Here's a shot of what mine looks like: (What you can see is C:\Users\Administrator\Documents\WindowsPowerShell\Modules)

My Modules folder

In there you can see the Reflection module and AutoLoad (which is another module it depends on). Apart from that you can see the Tridion core service module (and Pscx).

With all this in place, you are set to start writing your "straightforward" Tridion scripts. I've chosen to demonstrate this by hacking out a script that will create a default publication layout for you. It will be a handy tool to have on my research image, but mostly it's to show some real-world scripting.

param ($publicationPrefix = "")

$core = Get-TridionCoreServiceClient -protocol nettcp
import-module reflection
import-namespace Tridion.ContentManager.CoreService.Client

function createPublication {
	Param(
		[parameter(Mandatory=$true)]
		[ValidateNotNullOrEmpty()]
		[SessionAwareCoreServiceClient]$core, 
		[parameter(Mandatory=$true)]
		[ValidateNotNullOrEmpty()]
		[string]$title, 
		[string]$key, 
		[string[]]$parents,
		[switch]$Passthru
	)
	write-host "Creating publication $title"
	$newPublication = $core.GetDefaultData([ItemType]::Publication,"",$null)
	$newPublication.Title = $title
	if ($key -eq [string]::Empty){
		$newPublication.Key = $title
	}
	else {
		$newPublication.Key = $key
	}
	foreach ($parent in $parents){
		$link = new-object LinkToRepositoryData
		if ($parent -match "^tcm:"){
			$link.IdRef = $parent
		} elseif ($parent -match "^/webdav"){
			$link.WebDavUrl = $parent
		} else {
			continue
		}
		$newPublication.Parents += $link
	}
	if ($Passthru){
		$core.Create($newPublication, (new-object ReadOptions))
	}
	else {
		$core.Create($newPublication,$null)
	}
}

function createFolder([SessionAwareCoreServiceClient]$core, [string]$parentId, [string]$title, [switch]$Passthru){
	write-Host "Creating folder $title"
	$newFolder = $core.GetDefaultData([ItemType]::Folder, $parentId, $null)
	$newFolder.Title = $title
	if ($Passthru){
		$core.Create($newFolder, (new-object ReadOptions))
	}
	else {
		$core.Create($newFolder, $null)
	}
}

function createStructureGroup([SessionAwareCoreServiceClient]$core, [string]$parentId, [string]$title, [string]$directory, [switch]$Passthru){
	write-Host "Creating Structure Group $title"
	$newStructureGroup = $core.GetDefaultData([ItemType]::StructureGroup, $parentId, $null)
	$newStructureGroup.Title = $title
	$newStructureGroup.Directory = $directory
	if ($Passthru){
		$core.Create($newStructureGroup, (new-object ReadOptions))
	}
	else {
		$core.Create($newStructureGroup, $null)
	}
}

$chainMasterPub = createPublication $core "$($publicationPrefix)ChainMaster" -Passthru
$rsg = createStructureGroup $core $chainMasterPub.Id "root" "root" -Passthru

$definitionsPub = createPublication $core "$($publicationPrefix)Definitions" -parents @($chainMasterPub.Id) -Passthru
$systemFolder = createFolder $core  $definitionsPub.RootFolder.IdRef "System" -Passthru
createFolder $core $systemFolder.Id "Schemas"

$contentPub = createPublication $core "$($publicationPrefix)Content" -parents @($definitionsPub.Id) -Passthru
$contentFolder = createFolder $core $contentPub.RootFolder.IdRef "Content" -Passthru

$layoutPub = createPublication $core "$($publicationPrefix)Layout" -parents @($definitionsPub.Id) -Passthru
createFolder $core $core.GetTcmUri($systemFolder.Id, $layoutPub.Id, $null) "Templates"

createPublication $core "$($publicationPrefix)Web" -parents @($contentPub.Id,$layoutPub.Id)

The script accepts a parameter which lets me prefix the publications with some name relevant to whatever I'm doing, so if you invoke it like this:

PS C:\code\dominic\tridion> .\CreateDefaultStructure.ps1 "Apple"
Connecting to the Core Service at localhost...
Creating publication Apple 00 ChainMaster
Creating Structure Group root
Creating publication Apple 01 Definitions
Creating folder System
Creating folder Schemas
Creating publication Apple 02 Content
Creating folder Content
Creating publication Apple 03 Layout
Creating folder Templates
Creating publication Apple 04 Web
PS C:\code\dominic\tridion> .\CreateDefaultStructure.ps1 "Banana"
Connecting to the Core Service at localhost...
Creating publication Banana 00 ChainMaster
Creating Structure Group root
Creating publication Banana 01 Definitions
Creating folder System
Creating folder Schemas
Creating publication Banana 02 Content
Creating folder Content
Creating publication Banana 03 Layout
Creating folder Templates
Creating publication Banana 04 Web
PS C:\code\dominic\tridion>

... you end up with publications like this:

The resulting publications

 

I import the Tridion-CoreService module in my Powershell profile, so it's not needed in the script. (As noted earlier, my copy is a bit hacked, as you can see from the fact that I'm passing a protocol parameter to Get-TridionCoreServiceClient). I don't import the reflection module by default, so this is done in the script, followed immediately by "import-namespace Tridion.ContentManager.CoreService.Client", which is the magic from the Reflection module that wires up all the type accelerators. Once this is done, you can see that I can simply type [ReadOptions] instead of [Tridion.ContentManager.CoreService.Client.ReadOptions], and so on. Much better, I think! :-)

If you're wondering about the -Passthru switch on my functions, this is a powershell idiom that lets you indicate whether or not you are interested in the return value. In Tridion, this is controlled by whether or not you pass a ReadOptions argument. Perhaps obviously, the Read() method wouldn't make any sense if it didn't return anything, so a $null works fine - I'm still agonizing over whether it would be more stylish to pass a ReadOptions anyway. What do you think?)

Actually that's a good question. What do you think? I'm still trying to find my feet in terms of the correct idioms for this kind of work. Let's get the debate out in the open. Feel free to say mean things about my code (not obligatory). I've got a thick skin, and I'd genuinely value your feedback, especially if you think I'm doing it wrong.

Debugging 64 bit Tridion content delivery on IIS 7.5

I'm currently developing a web application which will run on Windows 2008 R2 and which is intended to run in a 64bit Application pool. This means that I'm running IIS 7.5, and that the web application is installed with the 64 bit versions of the Tridion content delivery assemblies. As you'll know if you've tried to run this kind of web application in a 32 bit process, you pretty soon get exceptions telling you that you have an invalid format. This gets a little inconvenient if you just start to debug your web application in Visual Studio. By default, if you have a page selected, and hit the big green Run triangle, the page will launch in IIS Express. If you have IIS 7.5, then IIS Express runs a 32 bit process, so the default setup just isn't going to work for you.

So - what to do? I had two options:

  1. Configure the properties of the web application to debug using IIS rather than IIS Express
  2. Launch the web page directly from the browser, and attach the debugger to the correct w3wp.exe process.

 

To be honest, the second of these was the choice that most matched my usual debugging approaches. Having said that, I did try the first approach, but so far without success. Visual Studio 2012 has frozen on me a few times while trying this. I'm interested if anyone has any tips on getting this working, but right now, I'm happy enough that I was able to succeed in attaching a debugger to w3wp.exe.

My biggest challenge was to figure out which process I wanted to attach to. On my development server, I have quite a few web sites running, and it's not altogether obvious which w3wp.exe to attach to. Attaching to them all might work in a trivial case, but realistically, it takes quite a while to load all the dlls, and adding any more processes than necessary is just going to hurt too much. So - how do you find out which process it is?

The first step is to ensure you have the IIS powershell provider installed on your server. These days, this is shipped as a module, so if it's available on your system, you should be able to open a powershell and type:

Get-Module -ListAvailable

If the response includes "WebAdministration" you are good to go. Just import the module as follows:

Import-Module WebAdministration

If this succeeds, you should be able to "change directory" into the IIS provider. (Although a PowerShell purist might prefer set-location... whatever floats your boat!)

cd IIS: 

If you can't find the module, then go into the Server manager, and check that you have the relevant role services for IIS installed. On other platforms, you might find that you can install it from the WebInstaller from the MSDN web site.

Now you're ready to find the process that you want to attach to: Assuming that your application pool is called "MyApplicationPool", then you can list its worker processes like this: (or use "dir" or "ls", either of which is an alias for "gci")

> gci IIS:\AppPools\MyApplicationPool\WorkerProcesses
Your output should look something like this:
Process  State      Handles  Start Time
Id
-------- -----      -------  ----------
2608     Running    776      1/2/2013 6:55:33 PM

This assumes, of course, that your app pool is actually running, but you'd have made sure it was before trying to debug it, right. Anyway - as you can see, the process id is there just to read off, and you can get straight on with your debugging session.

Toggling the javascript minification of the Tridion GUI from the powershell

Posted by Dominic Cronin at Nov 12, 2012 11:50 PM |
Filed under: , ,

Most of the time, I use a single Tridion development image for multiple purposes, including whatever time I get to spend researching how to do GUI extensions. When I'm flipping back out of research mode into doing some day-to-day development such as templating, it's better to have the benefit of the javascript minification that I might prefer to switch off while poking around in the guts of Anguilla. So just to make this switch as painless as possible, I've added the following code to my powershell $profile.

function SetGuiMinification($value){
  $filename = 'C:\Program Files (x86)\Tridion\web\WebUI\WebRoot\Configuration\System.config'
  $conf = [xml](gc $filename)
  $conf.Configuration.filters.filter |?{$_.type -like '*JScriptMinifier*'} |%{$_.enabled = $value}
  $conf.Save($filename)
  iisreset
}

function guimin {SetGuiMinification "always"}
function guinomin {SetGuiMinification "never"}

Now I can toggle backwards and forwards simply by typing guimin or guinomin (you may favour different words or spellings!)

Of course, this technique ought to work just as well to manipulate other elements and attributes in the XML files that control a Tridion installation. Perhaps you'd modify it to toggle the CSS minification too (removing the -like clause should do it).

If you have any good ideas for using this technique, please let me know.

How to list all the component templates associated with a schema

Posted by Dominic Cronin at Sep 26, 2012 06:45 PM |
Filed under: , ,

This posting might seem a little trivial, but having figured it out, I'm blogging it for my own reference. In fact, I was almost going to put it on the Tridion cookbook, but this is legacy stuff. There won't ever need to be a core service version of this, because in 2011, you can get the answer directly from Where Used.

But on older systems, say you wanted to update a schema, and wanted to figure out the impact on your templates. Which templates would you have to check for necessary updates, etc? (Imagine you were going to make a mandatory field optional, and wanted to check whether your templates would break if the user chose not to give a value.)

So you know which schema it is, and you want to know the component templates that have this as a related schema. I started to hack this out in Powershell using what are now for me fairly standard techniques. The trouble is that VBA collections are difficult to iterate over in the Powershell. Fortunately you can use the contains method on the RelatedSchemas collection to get the "where" clause you need. In most systems, you keep your templates, schemas etc, in a "system" folder, so the script simply starts at that location, and recursively grabs all the component templates it can find, If the schema of interest is in the related schemas, it will be listed.

$tdse = new-object -com TDS.TDSE
$interestingSchema = $tdse.Getobject("tcm:10-1234-8",1)
$systemFolder = $tdse.GetObject("tcm:11-123-2",1)
$rf = $tdse.CreateListRowFilter()
$rf.SetCondition("Recursive", $true)
$rf.SetCondition("ItemType", 32)
([xml]$systemFolder.GetListItems(3, $rf)).ListItems.Item | ?{$tdse.Getobject($_.ID,1).RelatedSchemas.Contains($interestingSchema)}

A poor man's Component synchroniser - or using the .NET framework to run XSLT from the PowerShell

Posted by Dominic Cronin at Aug 12, 2012 07:10 PM |

Just lately, I've been doing some work on porting the old Component Synchroniser power tool to the current version of Tridion. If you are familiar with the original implementation, you might know that it is based on a pretty advanced XSLT transformation (thankfully, that's not the part that needs porting), that pulls in data about the fields specified by the schema (including recursive evaluation of embedded schemas), and ensures that the component data is valid in terms of the schema. Quite often on an upgrade or migration project, any schema changes can be dealt with well enough by this approach, but sometimes you need to write a custom transformation to get your old component data to match the changes you've made in your schema. For example, the generic component synchroniser will remove any data that no longer has a field, but if you add a new field that needs to be populated on the basis of one of the old fields, you'll be reaching for your favourite XSLT editor and knocking up a migration transform.

This might sound like a lot of work, but very often, it isn't that painful. In any case, the XSLT itself is the hard part. The rest is just about having some boilerplate code to execute the transform. In the past, I've used various approaches, including quick-and-dirty console apps written in C#. As you probably know, in recent times, I've been a big fan of using the Windows Powershell to work with Tridion servers, and when I had to fix up some component migrations last week, of course, I looked to see whether it could be done with the PowerShell. A quick Google led me (as often happens!) to Scott Hanselman's site where he describes a technique using NXSLT2. Sadly, NXSLT2 now seems to be defunct, and anyway it struck me as perhaps inelegant, or at the least less PowerShell-ish to have to install another executable, when I already have the .NET framework,, with System.Xml.Xsl.XslCompiledTransform, available to me.

I've looked at doing XSLT transforms this way before, but there are so many overloads (of everything) that sometimes you end up being seduced by memory streams and 19 flavours of readers and writers. This time, I remembered System.IO.StringWriter, and the resulting execution of the transform took about four lines of code. The rest of what you see below is Tridion code that executes the transform against all the components based on a given schema. Sharp-eyed observers will note that in spite of a recent post here to the effect that I'm trying to wean myself from the TOM to the core service, this is TOM code. Yup - I was working on a Tridion 2009 server, so that was my only option. The good news is that the same PowerShell/XSLT technique will work just as well with the core service.

$tdse = new-object -com TDS.TDSE

$xslt = new-object System.Xml.XmlDocument
$xslt.Load("c:\Somewhere\TransformFooComponent.xslt")
$transform = new-object System.Xml.Xsl.XslCompiledTransform
$transform.Load($xslt)
$sb = new-object System.Text.StringBuilder
$writer = new-object System.IO.StringWriter $sb
filter FixFooComponent(){
$sb.Length = 0
$component = $tdse.GetObject($_, 2)
$xml = [xml]$component.GetXml(1919)
$transform.Transform($xml, $null, $writer)
$component.UpdateXml($sb.ToString())
$component.Save($true)
}
$schema = $tdse.GetObject("/webdav/SomePub/Building%20Blocks/System/Schemas/Foo.xsd",1)
([xml]$schema.Info.GetListUsingItems()).ListUsingItems.Item | ? {$_.Type -eq 16}| %{$_.ID} | FixFooComponent

A thing of beauty is a joy for ever

Posted by Dominic Cronin at Jul 09, 2012 09:30 PM |

So - I've been using the Windows Powershell for the odd bit of Tridion work. You knew that.

And you probably also knew that very often the Tridion API hands you back a string representing an XML document, and that it's very convenient to "cast" this to a .NET XmlDocument using the [xml] operator. Just search on this blog for "powershell" and you'll find enough examples. But still - there's a missing piece in the puzzle. So today I wanted to look at the output from the .GetTridionWebSchemaXMl() method on a Tridion Object Model Schema object. (Don't worry - I am weaning myself off the TOM; I wanted to compare this API with the ReadSchemaFields() method on the core service client API.)

Anyway - for what it's worth, here's what the raw string looks like:

> $tdse.GetObject("tcm:21-509-8",1).GetTridionWebSchemaXML(1919,$true)
<tcm:TridionWebSchema ID="tcm:21-509-8" IsEditable="false" xmlns:tcm="http://www.tridion.com/ContentManager/5.0"><tcm:C
ontext xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transform-e
xt="urn:tridion:transform-ext"><tcm:Publication xlink:type="simple" xlink:title="Synchroniser tests" xlink:href="tcm:0-
21-1" /><tcm:OrganizationalItem xlink:type="simple" xlink:title="TestSchemaOne" xlink:href="tcm:21-50-2" /></tcm:Contex
t><tcm:Info xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transf
orm-ext="urn:tridion:transform-ext"><tcm:LocationInfo><tcm:WebDAVURL>/webdav/Synchroniser%20tests/Building%20Blocks/Tes
tSchemaOne/TestSchemaCategories.xsd</tcm:WebDAVURL><tcm:Path>\Synchroniser tests\Building Blocks\TestSchemaOne</tcm:Pat
h></tcm:LocationInfo><tcm:BluePrintInfo><tcm:OwningPublication xlink:type="simple" xlink:title="Synchroniser tests" xli
nk:href="tcm:0-21-1" /><tcm:IsShared>false</tcm:IsShared><tcm:IsLocalized>false</tcm:IsLocalized></tcm:BluePrintInfo><t
cm:VersionInfo><tcm:Version>3</tcm:Version><tcm:Revision>0</tcm:Revision><tcm:CreationDate>2012-07-07T18:28:23</tcm:Cre
ationDate><tcm:RevisionDate>2012-07-09T20:18:21</tcm:RevisionDate><tcm:Creator xlink:type="simple" xlink:title="TRIDION
DEV\Administrator" xlink:href="tcm:0-11-65552" /><tcm:Revisor xlink:type="simple" xlink:title="TRIDIONDEV\Administrator
" xlink:href="tcm:0-11-65552" /><tcm:ItemLock Title="No lock" Type="0" /><tcm:IsNew>false</tcm:IsNew></tcm:VersionInfo>
<tcm:AllowedActions><tcm:Actions Allow="1173513" Deny="102" Managed="0" /></tcm:AllowedActions></tcm:Info><tcm:Data xml
ns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transform-ext="urn:tr
idion:transform-ext"><tcm:Title>TestSchemaCategories</tcm:Title><tcm:Description>TestSchemaCategories</tcm:Description>
<tcm:Purpose>Component</tcm:Purpose><tcm:NamespaceURI>uuid:f14d60ed-0f7c-4d1f-a2e3-97d1dfeb1a1f</tcm:NamespaceURI><tcm:
RootElementName>Content</tcm:RootElementName><tcm:Fields><tcm:KeywordField><tcm:Name>ColoursOne</tcm:Name><tcm:Descript
ion>ColoursOne</tcm:Description><tcm:MinOccurs>1</tcm:MinOccurs><tcm:MaxOccurs>unbounded</tcm:MaxOccurs><tcm:Category x
link:type="simple" xlink:title="Colours" xlink:href="tcm:21-59-512" /><tcm:Size>1</tcm:Size><tcm:List Type="tree" /><tc
m:ExtensionXml xmlns="http://www.sdltridion.com/ContentManager/R6" /></tcm:KeywordField><tcm:SingleLineTextField><tcm:N
ame>Animals</tcm:Name><tcm:Description>Test field with locally declared list</tcm:Description><tcm:MinOccurs>1</tcm:Min
Occurs><tcm:MaxOccurs>1</tcm:MaxOccurs><tcm:Size>1</tcm:Size><tcm:List Type="select"><tcm:Entry>Horse</tcm:Entry><tcm:E
ntry>Haddock</tcm:Entry><tcm:Entry>Weasel</tcm:Entry></tcm:List><tcm:ExtensionXml xmlns="http://www.sdltridion.com/Cont
entManager/R6" /></tcm:SingleLineTextField></tcm:Fields><tcm:MetadataFields /><tcm:AllowedMultimediaTypes /><tcm:Compon
entProcess xlink:type="simple" xlink:title="" xlink:href="tcm:0-0-0" /></tcm:Data></tcm:TridionWebSchema>

Yeah - erm ... Okaayyyy. Great.

OK - so how about we do the cast?

 

> [xml]$tdse.GetObject("tcm:21-509-8",1).GetTridionWebSchemaXML(1919,$true)
TridionWebSchema
----------------
TridionWebSchema

 

Well - at least you can read it.. but seriously - also not super helpful if you just want to scan the XML with good-old-fashioned human eyeballs.

So what can we do? Well I got to the point where I actually typed the following into Google:

powershell pretty print xml

and the first hit was on Keith Hill's blog.  Keith had written a nice little function that looks like this:

function XmlPrettyPrint([string]$xml) {
    $tr = new-object System.IO.StringReader($xml)
    $settings = new-object System.Xml.XmlReaderSettings
    $settings.CloseInput = $true
    $settings.IgnoreWhitespace = $true
    $reader = [System.Xml.XmlReader]::Create($tr, $settings)
    $sw = new-object System.IO.StringWriter
    $settings = new-object System.Xml.XmlWriterSettings
    $settings.CloseOutput = $true
    $settings.Indent = $true
    $writer = [System.Xml.XmlWriter]::Create($sw, $settings)
    
    while (!$reader.EOF) {
        $writer.WriteNode($reader, $false)
    }
    $writer.Flush()
    
    $result = $sw.ToString()
    $reader.Close()
    $writer.Close()
    $result
}

A minute later, this function was in my Powershell profile (and I slightly altered the name and added an alias) so now I can do the following:

> ppx ([xml]$tdse.GetObject("tcm:21-509-8",1).GetTridionWebSchemaXML(1919,$true)).OuterXml
<?xml version="1.0" encoding="utf-16"?>
<tcm:TridionWebSchema ID="tcm:21-509-8" IsEditable="false" xmlns:tcm="http://www.tridion.com/ContentManager/5.0">
  <tcm:Context xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transform-ext="urn:tridion:transform-ext">
    <tcm:Publication xlink:type="simple" xlink:title="Synchroniser tests" xlink:href="tcm:0-21-1" />
    <tcm:OrganizationalItem xlink:type="simple" xlink:title="TestSchemaOne" xlink:href="tcm:21-50-2" />
  </tcm:Context>
  <tcm:Info xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transform-ext="urn:tridion:transform-ext">
    <tcm:LocationInfo>
      <tcm:WebDAVURL>/webdav/Synchroniser%20tests/Building%20Blocks/TestSchemaOne/TestSchemaCategories.xsd</tcm:WebDAVURL>
      <tcm:Path>\Synchroniser tests\Building Blocks\TestSchemaOne</tcm:Path>
    </tcm:LocationInfo>
    <tcm:BluePrintInfo>
      <tcm:OwningPublication xlink:type="simple" xlink:title="Synchroniser tests" xlink:href="tcm:0-21-1" />
      <tcm:IsShared>false</tcm:IsShared>
      <tcm:IsLocalized>false</tcm:IsLocalized>
    </tcm:BluePrintInfo>
    <tcm:VersionInfo>
      <tcm:Version>3</tcm:Version>
      <tcm:Revision>0</tcm:Revision>
      <tcm:CreationDate>2012-07-07T18:28:23</tcm:CreationDate>
      <tcm:RevisionDate>2012-07-09T20:18:21</tcm:RevisionDate>
      <tcm:Creator xlink:type="simple" xlink:title="TRIDIONDEV\Administrator" xlink:href="tcm:0-11-65552" />
      <tcm:Revisor xlink:type="simple" xlink:title="TRIDIONDEV\Administrator" xlink:href="tcm:0-11-65552" />
      <tcm:ItemLock Title="No lock" Type="0" />
      <tcm:IsNew>false</tcm:IsNew>
    </tcm:VersionInfo>
    <tcm:AllowedActions>
      <tcm:Actions Allow="1173513" Deny="102" Managed="0" />
    </tcm:AllowedActions>
  </tcm:Info>
  <tcm:Data xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:transform-ext="urn:tridion:transform-ext">    <tcm:Title>TestSchemaCategories</tcm:Title>
    <tcm:Description>TestSchemaCategories</tcm:Description>
    <tcm:Purpose>Component</tcm:Purpose>
    <tcm:NamespaceURI>uuid:f14d60ed-0f7c-4d1f-a2e3-97d1dfeb1a1f</tcm:NamespaceURI>
    <tcm:RootElementName>Content</tcm:RootElementName>
    <tcm:Fields>
      <tcm:KeywordField>
        <tcm:Name>ColoursOne</tcm:Name>
        <tcm:Description>ColoursOne</tcm:Description>
        <tcm:MinOccurs>1</tcm:MinOccurs>
        <tcm:MaxOccurs>unbounded</tcm:MaxOccurs>
        <tcm:Category xlink:type="simple" xlink:title="Colours" xlink:href="tcm:21-59-512" />
        <tcm:Size>1</tcm:Size>
        <tcm:List Type="tree" />
        <tcm:ExtensionXml xmlns="http://www.sdltridion.com/ContentManager/R6" />
      </tcm:KeywordField>
      <tcm:SingleLineTextField>
        <tcm:Name>Animals</tcm:Name>
        <tcm:Description>Test field with locally declared list</tcm:Description>
        <tcm:MinOccurs>1</tcm:MinOccurs>
        <tcm:MaxOccurs>1</tcm:MaxOccurs>
        <tcm:Size>1</tcm:Size>
        <tcm:List Type="select">
          <tcm:Entry>Horse</tcm:Entry>
          <tcm:Entry>Haddock</tcm:Entry>
          <tcm:Entry>Weasel</tcm:Entry>
        </tcm:List>
        <tcm:ExtensionXml xmlns="http://www.sdltridion.com/ContentManager/R6" />
      </tcm:SingleLineTextField>
    </tcm:Fields>
    <tcm:MetadataFields />
    <tcm:AllowedMultimediaTypes />
    <tcm:ComponentProcess xlink:type="simple" xlink:title="" xlink:href="tcm:0-0-0" />
  </tcm:Data>
</tcm:TridionWebSchema>

So what's my point? Well I have a couple:

  1. The Internet is great (by which I mean, the people of the Internet are great.). I could have written that function myself in about half an hour. But in practice I might not have had the energy to do so at 10pm on a working Monday. Thanks to Keith's willingness to share, I had my solution inside a minute - working and tested. How cool is that?
  2. Somehow, this has just taken a little bit of the friction out of my working day, not to mention my so-called free time. I can now pull data straight out of a method that returns string, and get human-readable XML. This stuff makes a difference.

 

Thanks Keith - and all the other Keith's out there.

P.S. Maybe even nicely formatted XML will never be a thing of beauty, so apologies to Keats.

Getting to grips with the Tridion core service in Powershell

Posted by Dominic Cronin at Apr 01, 2012 05:05 PM |

As regular readers of this blog will know, I've been a long-standing fan of the Windows Powershell as a tool for interacting with Tridion. On more than one project, the flexibility of the Powershell has allowed me to process Tridion data in ad-hoc ways that would be unthinkable if you had to bring with you all the overhead of, say, C# and Visual Studio. All of that is, of course, positive, but the downside of it has been that I don't seem to be making the jump over to the core service, which, after all, I should expect to be one of my primary APIs for some time to come. So time to make a change.

A while ago, I had tinkered with using the TOM.NET API from the Powershell, but I stopped putting effort into that once I got the basics working. The advice from SDL is clearly to use the core service for the kind of scenarios that the Powershell covers. Just for the record, though - getting a TOM.NET session in the Powershell is considerably more difficult than the equivalent activity using the core service. To be fair, neither technique even remotely approaches the simplicity of "$tdse = new-object -com TDS.TDSE", but like I said, it's time to move on.

So once I started looking at this, I had a quick look at Frank van Puffelen's GetCoreServiceClientWithoutConfigFile recipe in the Tridion Cookbook, and then I spent some time snuffling around in Peter Kjaer's Tridion Powershell Modules. Both of these are great resources, but I suffer quite badly from Not Invented Here syndrome, so at the very least, I had to poke around a bit and see what's going on. After some blatant stealing: mostly from Peter's code, I ended up with this:

Add-Type -assemblyName System.ServiceModel
$binding = new-object System.ServiceModel.WsHttpBinding
$binding.MaxBufferPoolSize = [int]::MaxValue
$binding.MaxReceivedMessageSize = [int]::MaxValue
$binding.ReaderQuotas.MaxArrayLength = [int]::MaxValue
$binding.ReaderQuotas.MaxBytesPerRead = [int]::MaxValue
$binding.ReaderQuotas.MaxNameTableCharCount = [int]::MaxValue
$binding.ReaderQuotas.MaxStringContentLength = [int]::MaxValue
$endpoint = new-object System.ServiceModel.EndpointAddress http://localhost/webservices/CoreService2011.svc/wsHttp
Add-Type -Path 'C:\Program Files (x86)\Tridion\bin\client\Tridion.ContentManager.CoreService.Client.dll'
$core = new-object Tridion.ContentManager.CoreService.Client.SessionAwareCoreServiceClient $binding,$endpoint

So what's going on here? What I've extracted is pretty close to the barest minimum implementation I could get to. Maybe you could get it smaller if you weren't bothered by running up against the fairly low default quota values offered by the Windows Communication Framework. In fact, I'm quite unsure about my approach to the quotas. What I've done is effectively to say that quotas aren't helpful for my scenario, and set them all to the maximum possible. Does this make sense? Let me know what you think. (Edit: I asked a question about this on stackoverflow, and got some good answers.)

So - to use the service you need three things:

  1. The core service client assembly. (It's great that SDL are now shipping this with the product. This means I can mail you a script, and say "use the 'official' client", and expect it to work.)
  2. A System.ServiceModel.WsHttpBinding object
  3. A System.ServiceModel.EndpointAddress object

So we load the System.ServiceModel assembly using Powershell's Add-Type cmdlet. This assembly is part of the .NET framework so we can just ask for it by name. Later in the script we use the same cmdlet to load the client dll, but then we have to specify its location. Once we have System.ServiceModel loaded, we can instantiate a binding and an endpoint, and pass those to the constructor of the client. Even though we end up with a few lines of code, it's not really hard, eh?

From here on we can just use the $core object to talk to the service. To be honest, having had a bit of a dig into how it works, you're probably better off just using Peter's module, which takes care of more than my hard-coded version does, and also offers some utility methods, for example, to create a new user. In fact, assuming you have installed the module, getting started is even easier than instantiating a TDSE: just "$core = Get-TridionCoreServiceClient". Nice job, Peter. Thanks.

Edit: If you prefer a NetTcp binding, this is pretty simple too: Just instantiate the correct binding type:

$binding = new-object System.ServiceModel.NetTcpBinding

And use a different endpoint

$endpoint = new-object System.ServiceModel.EndpointAddress net.tcp://localhost:2660/CoreService/2011/netTcp

Batching components for the component synchronizer

Posted by Dominic Cronin at Feb 15, 2012 07:25 PM |

Although the Tridion power tools are currently undergoing a major overhaul to bring them in line with SDL Tridion 2011 and its Anguilla extensibility framework, many of us will be working on pre-2011 systems for some time to come. This being so, the "old" power tools remain a useful resource. Today I was working on a 2009 system, where we are busy with a data migration. This means using the component synchronizer. We've been using it for a few things, but today we wanted to remove some redundant fields from a schema, and then have that reflected in the data. The schema in question had about 8500 components based on it, and processing the entire list in one go was going to slow the whole team down. (We're doing this on a relatively under-powered image that can run quite slowly sometimes - it's easy to max it out!)

So what to do? The way of using the component synchronizer that I'm most familiar with is simply to select the schema and then process all it's components. However, the synchronizer also has an option to process a specific list of components; you have to paste a comma-separated list of TCM URIs into a text box. So then the question was how to get such lists with a reasonable amount of effort. Powershell to the rescue. Here's the approach:

 > $tdse = new-object -com TDS.TDSE
> $alg = $tdse.GetObject("tcm:12-1255-8",1)
> $f = $tdse.CreateListRowFilter()
> $f.SetCondition("ItemType", 16)
> $docAlgItems = [xml]$alg.Info.GetListUsingItems(1,$f)
> $AlgTcms = $docAlgItems.ListUsingItems.Item | %{$_.ID}
> $algTcms[0..999] -join "," | out-file first1000.txt
> notepad .\first1000.txt

As you can see, we start with instantiating TDSE and getting hold of the schema. To protect the innocent, let's pretend that this schema is called Algernon. $alg is the variable representing the schema.

So - after a quick where-used, we do the standard Powershell trick of casting the results to an XmlDocument and reading off the ID attributes. By this time, $AlgTcms contains an array of TCM URIs and it's almost trivial to dump the first thousand into a file by specifying an array range (obviously you can get the second thousand by saying $AlgTcms[1000..1999] and so on) and doing a -join

So instead of maxing out the system for several hours, we were able to schedule the work in reasonable batches through the day.

Smoke-testing my Tridion database connections

Posted by Dominic Cronin at Jan 27, 2012 10:15 PM |
Filed under: , ,

I'm installing and configuring Tridion 2011 SP1. There are now so many databases, that it's just insane to try to keep track of them all by hand, but nil desperandum, the power shell is here. OK - you might not be quite so compulsive/obsessive, but I threw together a script that lets me have a list of verified working logins before I start poking at config files. At the very least, it brings out some findings about consistency across the different products. Here's what I did:

function CheckDatabase($connStringBuilder, $queryString="select DB_VERSION from TDS_DB_INFO", $CommandType="Text"){
  $conn = new-object System.Data.SqlClient.SqlConnection
  $conn.ConnectionString = $connStringBuilder.ConnectionString

  $conn.Open()

  $comm = new-object System.Data.SqlClient.SqlCommand
  $comm.CommandText = $queryString
  $comm.CommandType = $CommandType
  $comm.Connection = $conn
  $reader = $comm.ExecuteReader() 
  $readResult = $reader.Read() 
  $dbversion = $reader.GetString(0)
  $reader.Close()
  $Conn.Close()
  $dbName = $connStringBuilder["Initial Catalog"]
  if ($dbversion.length -gt 0) {"$dbname Database version found: $dbversion"} else {"$dbname fffft"}
}


$connStringBuilder = new-object System.Data.SqlClient.SqlConnectionStringBuilder
$connStringBuilder["Data Source"] = "MY_LAPTOP\DEVELOPER"
$connStringBuilder["Initial Catalog"] = "Tridion_cm"
$connStringBuilder["User ID"] = "TCMDBUSER"
$connStringBuilder["Password"] = "Yes I used the same password for all dbs - don't you?"

CheckDatabase $connStringBuilder


$connStringBuilder["Initial Catalog"] = "Tridion_Broker"
$connStringBuilder["User ID"] = "TridionBrokerUser"

CheckDatabase $connStringBuilder

$connStringBuilder["Initial Catalog"] = "Tridion_cm_email"
$connStringBuilder["User ID"] = "TMSDBUSER"

CheckDatabase $connStringBuilder "select DB_VERSION from OE_DB_INFO"

$connStringBuilder["Initial Catalog"] = "Tridion_submgmt"
$connStringBuilder["User ID"] = "TMSSMUSER"

CheckDatabase $connStringBuilder "select DB_VERSION from DB_INFO"

$connStringBuilder["Initial Catalog"] = "Tridion_tracking"
$connStringBuilder["User ID"] = "TMSPSUSER"

CheckDatabase $connStringBuilder "PS_READ_DBINFO" "StoredProcedure"

$connStringBuilder["Initial Catalog"] = "Tridion_TranslationManager" 
$connStringBuilder["User ID"] = "TMUser"

CheckDatabase $connStringBuilder "SELECT DB_VERSION FROM TM_DB_INFO"

$connStringBuilder["Initial Catalog"] = "Tridion_Ugc"
$connStringBuilder["User ID"] = "TridionUgcUser"

CheckDatabase $connStringBuilder "SELECT DB_VERSION FROM UGC_TDS_DB_INFO"

And here's what the output looked like:

 . C:\Users\Administrator\Desktop\dbTest.ps1
Tridion_cm Database version found: 6.1.0.0
Tridion_Broker Database version found: 6.1.0.0
Tridion_cm_email Database version found: 2.2.0.0
Tridion_submgmt Database version found: 2.2.0.0
Tridion_tracking Database version found: 2.2.0.0
Tridion_TranslationManager Database version found: 3.0.0.0
Tridion_Ugc Database version found: 6.1.0.0

All in all - maybe not worth the effort, but somehow satisfying. Is it useful? Maybe.

Using powershell to do useful things with XML lists from Tridion

Posted by Dominic Cronin at Dec 30, 2010 08:55 PM |
Filed under: , ,

For a while now I've been trying to persuade anyone that would listen that Windows Powershell is great for interacting with the Tridion object model (TOM). What I mean by this is that you can easily use the powershell to instantiate and use COM objects, and more specifically, TOM objects. In this post, I'm going to take it a bit further, and show how you can use the powershell's XML processing facilities to easily process the lists that are available from the TOM as XML Documents. The example I'm going to use is a script to forcibly finish all the workflow process instances in a Tridion CMS. (This is quite useful if you are doing workflow development, as you can't upload a new version of a workflow while there are process instances that still need to be finished.)

Although useful, the example itself isn't so important. I'm simply using it to demonstrate how to process lists. Tridion offers several API calls that will return a list, and in general, the XML looks very similar. I'm going to aim to finish all my process instances as a "one-liner", although I'm immediately going to cheat by setting up the necessary utility objects as shell variables:

> $tdse = new-object -com TDS.TDSE
> $wfe = $tdse.GetWFE()

As you can see, I'm using the new-object cmdlet to get a TDSE object, specifying that it is a COM object (by default new-object assumes you want a .NET object). Then I'm using $tdse to get the WFE object which offers methods that list workflow items. With these two variables in place, I can attempt my one liner. Here goes:

> ([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item | % {$tdse.GetObject($_.ID,2)} | % {$_.FinishProcess()}

Well, suffice it to say that this works, and once you've run it (assuming you are an admin), you won't have any process instances, but perhaps we need to break it down a bit....

If you start off with just $wfe.GetListProcessInstances(), the powershell will invoke the method for you, and return the XML as a string, which is what GetListProcessInstances returns. Just like this:

> $wfe.GetListProcessInstances()
<?xml version="1.0"?>
<tcm:ListWFProcessInstances xmlns:tcm="http://www.tridion.com/ContentManager/5.0" xmlns:xlink="http://www.w3.org/1999/x
link"><tcm:Item ID="tcm:24-269-131076" PublicationTitle="300 Global Content (NL)" TCMItem="tcm:24-363" Title="Test 1" T
CMItemType="16" ProcessDefinitionTitle="Application Content Approval" ApprovalStatus="Unapproved" ActivityDefinitionTyp
e="1" WorkItem="tcm:24-537-131200" CreationDate="2010-12-30T19:35:33" State="Started" Icon="T16L1P0" Allow="41955328" D
eny="16777216"/><tcm:Item ID="tcm:24-270-131076" PublicationTitle="300 Global Content (NL)" TCMItem="tcm:24-570" Title=
"Test 2" TCMItemType="16" ProcessDefinitionTitle="Application Content Approval" ApprovalStatus="Unapproved" ActivityDef
initionType="1" WorkItem="tcm:24-538-131200" CreationDate="2010-12-30T19:36:04" State="Started" Icon="T16L1P0" Allow="4
1955328" Deny="16777216"/></tcm:ListWFProcessInstances>

OK - that's great - if you dig into it, you'll see that there is a containing element called ListWFProcessInstances, and that what it contains are some Item elements. All of this is in the tcm namespace, and each Item has various attributes. Unfortunately, the XML in this form is ugly and not particularly useful. Fortunately, the powershell has some built-in features that help quite a lot with this. The first is that if you use the [xml] cast operator, the string is transformed into a System.Xml.XmlDocument. To test this, just assign the result of the cast to a variable and use the get-member cmdlet to display it's type and methods:

> $xml = [xml]$wfe.GetListProcessInstances()
> $xml | gm

(Of course, you don't type "get-member". "gm" is sufficient - most standard powershell cmdlets have consistent and memorable short aliases.)

I won't show the output here, as it fills the screen, but at the top, the type is listed, and then you see the API of System.Xml.XmlDocument. (Actually you don't need a variable here, but it's nice to have a go and use some of the API methods.)

All this would be pretty useful even if it stopped there, but it gets better. Because the powershell is intended as a scripting environment, the creators have wrapped an extra layer of goodness around XmlDocument. The assumption is that you probably want to extract some values without having to write XPaths, instantiate Node objects and all that other nonsense, so they let you access Elements and Attributes by magicking up collections of properties. Using the example above, I can simply type the names of the Elements and Attributes I want in a "dot-chain". For example:

> ([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item[0].ID
tcm:24-269-131076

Here you can also see that I'm referencing the first Item element in the collection and getting its ID attribute. The tcm ID is returned. All this is great for exploring the data interactively, but be warned, there is a fly in the ointment. Behind the scenes, the powershell uses its own variable called Item to represent the members of the collections it creates. This means that whereas you ought to be able to type

([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances

and get some meaningful output, instead, you'll get an error saying:

format-default : The member "Item" is already present.
    + CategoryInfo          : NotSpecified: (:) [format-default], ExtendedTypeSystemException
    + FullyQualifiedErrorId : AlreadyPresentPSMemberInfoInternalCollectionAdd,Microsoft.PowerShell.Commands.FormatDefaultCommand

This is because Tridion's list XML uses "Item" for the element name, and it conflicts with powershell's own use of the name. It's an ugly bug in powershell, but fortunately it doesn't affect us much. Instead of saying "ListWFProcessInstances", just keep on typing and say "ListWFProcessInstances.Item" and you are back in the land of sanity.

Apart from this small annoyance, the powershell offers superb discoverability, so for example, it will give you tab completion so that you don't even have to know the name of ListWFProcessInstances. If at any point you are in doubt as to what to type next, just stop where you are and pipe the result into get-member - all will be revealed.

OK - back to the main plot. If you're with me this far, you have probably realised that

([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item

will get you a collection of objects representing the Item elements in the XML. As you probably know, an important feature of powershell is that you can pipeline collections of objects, and that there is syntax built in for processing them. The % character is used as shorthand for foreach, and within the foreach block (delimited by braces), the symbol $_ represents the current item in the iteration. For example, we could write:

> ([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item | % {$_.ID}

and get the output:

tcm:24-269-131076
tcm:24-270-131076

I'm sure you can see where this is going. We need to transform the collection of XML attributes: the IDs of the process instances, into a collection of TOM objects, so with a small alteration in the body of the foreach block, we have

% {$tdse.GetObject($_.ID,2)}

and then we can pipe the resulting collection of TOM objects into a foreach block which invokes the FinishProcess() method:

 

% {$_.FinishProcess()}

Of course, if you like really terse one-liners, you could amalgamate the last two pipeline elements so that instead of:

> ([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item | % {$tdse.GetObject($_.ID,2)} | % {$_.FinishProcess()}

we get:

> ([xml]$wfe.GetListProcessInstances()).ListWFProcessInstances.Item | % {$tdse.GetObject($_.ID,2).FinishProcess()} 

but in practice, you develop these one-liners by exploration, and if you want something really terse, you are more likely to write a more long-hand version, put it in your $profile, and give it an alias.

As I said at the top - this is just an example. All the TOM functions that return XML lists can be treated in a similar manner. Generally all that changes is the name of the root element of the XML document, and as I have pointed out, this is easily discoverable.

I hope this approach proves useful to you. If you have any examples of good applications, please let me know in the comments.

A Happy New Year to you all.

Dominic