Sunday, July 25, 2010

Using NVelocity installed into GAC with example for Sharepoint

In this post I will describe how you can use NVelocity template engine in Sharepoint and what issues will occur with it. Main problem is that NVelocity works well if it is installed in bin folder of you web application. But in Sharepoint development most common case when assemblies are installed into GAC. Ofcourse you can still add NVelocity.dll into bin folder of target Sharepoint web site (if you use WSPBuilder see this post which describes how to install dll into bin folder instead of GAC). But what if you need still install assemblies into GAC (e.g. because of security reasons)? Unfortunately by default NVelocity can not be used from GAC. I will show how to fix this problem for such scenario. Although I will use examples for Sharepoint this post can be used in other cases when you need to use NVelocity from GAC.

First of all a few words about NVelocity. It is template engine ported from Java world into .Net (when I talk about NVelocity I mean implementation from Castle project instead original version – there are several convenient improvements which will be described below). It is quite useful for many scenarios when you need to generate template-based content using set of parameters. For example you need to send emails which are generated based on template. In this case you can use create custom template with placeholders (using curly brackets) and replace them in runtime using regular expressions for example. It will work, but what if you need add iterative blocks into your template? You will need to add this functionality into your custom template engine, fixing more bugs, add more test, etc. Why do we need to create own implementation of things which are already exists? Lets see how NVelocity may help.

It is quite powerful framework which has native supports of loops inside template. Also with NVelocity you can use C# like syntax for your templates. Lets see example. Suppose that we have the following model classes for our email:

   1: public class Letter
   2: {
   3:     public Header Header { get; set; }
   4:     public List<Block> Blocks { get; set; }
   5: }
   6:  
   7: public class Block
   8: {
   9:     public string Title { get; set; }
  10:     public string Content { get; set; }
  11: }
  12:  
  13: public class Header
  14: {
  15:     public string Title { get; set; }
  16:     public string Image { get; set; }
  17: }

Letter has header and several blocks. Now create template for this email using NVelocity:

   1: <html>
   2:   <head>
   3:   </head>
   4:   <body>
   5:     <table>
   6:       <tr>
   7:         <td><img src="$letter.Header.Image" /></td>
   8:         <td>$letter.Header.Title</td>
   9:       </tr>
  10:  
  11:       #foreach($block in $letter.Blocks)
  12:       #before
  13:       <tr>
  14:       #each
  15:         <td>$block.Title</td>
  16:         <td>$block.Content</td>
  17:       #after
  18:        </tr>
  19:       #end
  20:  
  21:     </table>
  22:   </body>
  23: </html>

Here you can find more detailed description of NVelocity improvements made in Castle project. As you can see template code is quite straightforward as we use C# like syntax. We used nested properties $letter.Header.Title and foreach macro for expanding list of blocks. In order to generate email based on this template you can use the following code:

   1: var velocity = new VelocityEngine();
   2: var props = new ExtendedProperties();
   3: string templateFolder = ...; // physical path to the folder with template files
   4: props.AddProperty("file.resource.loader.path", templateFolder);
   5:  
   6: velocity.Init(props);
   7:  
   8: var template = velocity.GetTemplate("template.vm");
   9: var context = new VelocityContext();
  10: context.Put("letter", letter);
  11:  
  12: using (var writer = new StringWriter())
  13: {
  14:     template.Merge(context, writer);
  15:     string result = writer.GetStringBuilder().ToString();
  16:     return result;
  17: }

If you use this code in Sharepoint you template files can be located in 12/template/layouts folder. But there is another problem if you install NVelocity.dll into GAC. Running this code the following exception will be thrown:

The specified class for Resourcemanager (NVelocity.Runtime.Resource.ResourceManagerImpl,NVelocity) does not exist.

There are lot of mentions of this problem in internet (e.g. here) but I didn’t find any working complete solution. As you can see NVelocity doesn’t use assembly strong name for type resolution. So if assembly is installed into GAC, this type can not be resolved. Fortunately there are several extension points in NVelocity which can be used to resolve this and several other similar exceptions. We need to add the following code into NVelocity initialization process:

   1: var velocity = new VelocityEngine();
   2: var props = new ExtendedProperties();
   3: string templateFolder = ...;
   4: props.AddProperty("file.resource.loader.path",
   5:     templateFolder);
   6: props.AddProperty("resource.manager.class",
   7:     typeof(ResourceManagerImpl).AssemblyQualifiedName);
   8: props.AddProperty("file.resource.loader.class",
   9:     typeof(FileResourceLoader).AssemblyQualifiedName);
  10: props.AddProperty("directive.manager",
  11:     typeof(DirectiveManager).AssemblyQualifiedName);
  12:  
  13: velocity.Init(props);

Unfortunately this is not enough as after you added this code you will get another exception:

Could not resolve type NVelocity.Runtime.Directive.Include,NVelocity
   at NVelocity.Runtime.Directive.DirectiveManager.Register(String directiveTypeName)
   at NVelocity.Runtime.RuntimeInstance.initializeDirectives()
   at NVelocity.Runtime.RuntimeInstance.Init()
   at NVelocity.Runtime.RuntimeInstance.Init(ExtendedProperties p)
   at NVelocity.App.VelocityEngine.Init(ExtendedProperties p)

In order to resolve this error we need to dig into NVelocity source code located currently at GitHub and change it. Lets see falling code of RuntimeInstance.initializeDirectives() method:

   1: private void initializeDirectives()
   2: {
   3:     initializeDirectiveManager();
   4:  
   5:     ExtendedProperties directiveProperties = new ExtendedProperties();
   6:  
   7:     try
   8:     {
   9:         directiveProperties.Load(
  10:             Assembly.GetExecutingAssembly().GetManifestResourceStream(
  11:                 RuntimeConstants.DEFAULT_RUNTIME_DIRECTIVES));
  12:     }
  13:     catch(System.Exception ex)
  14:     {
  15:         throw new System.Exception(
  16:             string.Format(
  17:                 "Error loading directive.properties!...\n{0}",
  18:                 ex.Message));
  19:     }
  20:  
  21:     IEnumerator directiveClasses = directiveProperties.Values.GetEnumerator();
  22:  
  23:     while(directiveClasses.MoveNext())
  24:     {
  25:         String directiveClass = (String) directiveClasses.Current;
  26:         directiveManager.Register(directiveClass);
  27:     }
  28:  
  29:     String[] userdirective = configuration.GetStringArray("userdirective");
  30:     for(int i = 0; i < userdirective.Length; i++)
  31:     {
  32:         directiveManager.Register(userdirective[i]);
  33:     }
  34: }

So it reads values from manifest resource stream (manifest resource name DEFAULT_RUNTIME_DIRECTIVES = NVelocity.Runtime.Defaults.directive.properties) which contains list of classes and creates instances of these classes. File with these classes is located in Runtime/Defaults/directive.properties file in NVelocity solution:

   1: directive.1=NVelocity.Runtime.Directive.Foreach\,NVelocity
   2: directive.2=NVelocity.Runtime.Directive.Include\,NVelocity
   3: directive.3=NVelocity.Runtime.Directive.Parse\,NVelocity
   4: directive.4=NVelocity.Runtime.Directive.Macro\,NVelocity
   5: directive.5=NVelocity.Runtime.Directive.Literal\,NVelocity

As you can see it doesn’t contains assembly strong name. In order to fix mentioned problem we need to modify this file by adding strong name:

   1: directive.1=NVelocity.Runtime.Directive.Foreach\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   2: directive.2=NVelocity.Runtime.Directive.Include\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   3: directive.3=NVelocity.Runtime.Directive.Parse\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   4: directive.4=NVelocity.Runtime.Directive.Macro\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   5: directive.5=NVelocity.Runtime.Directive.Literal\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc

We also need to modify RuntimeInstance.initializeDirectives() method in order to allow NVelocity to read assemblies strong names:

   1: private void initializeDirectives()
   2: {
   3:     initializeDirectiveManager();
   4:  
   5:     ExtendedProperties directiveProperties = new ExtendedProperties();
   6:  
   7:     try
   8:     {
   9:         directiveProperties.Load(
  10:             Assembly.GetExecutingAssembly().GetManifestResourceStream(
  11:                 RuntimeConstants.DEFAULT_RUNTIME_DIRECTIVES));
  12:     }
  13:     catch(System.Exception ex)
  14:     {
  15:         throw new System.Exception(
  16:             string.Format(
  17:                 "Error loading directive.properties!...\n{0}",
  18:                 ex.Message));
  19:     }
  20:  
  21:     IEnumerator directiveClasses = directiveProperties.Values.GetEnumerator();
  22:  
  23:     while(directiveClasses.MoveNext())
  24:     {
  25:         ArrayList tokens = (ArrayList)directiveClasses.Current;
  26:         string[] typeTokens =
  27:             tokens.Cast<object>().Select(o => (string)o).ToArray();
  28:         string directiveClass = string.Join(",", typeTokens);
  29:  
  30:         directiveManager.Register(directiveClass);
  31:     }
  32:  
  33:     String[] userdirective = configuration.GetStringArray("userdirective");
  34:     for(int i = 0; i < userdirective.Length; i++)
  35:     {
  36:         directiveManager.Register(userdirective[i]);
  37:     }
  38: }

Now recompile NVelocity and reinstall it in GAC. After this you will be able to use it from GAC.

7 comments:

  1. hi roeladams,
    I uploaded it here http://www.box.net/shared/fkj3kgoada

    ReplyDelete
  2. in order to use .Cast<> extension method you need to add:
    using System.Linq;
    on top of .cs file

    ReplyDelete
  3. roeladams,
    no I didn't use hashtables inside templates. Instead of hastables I used strongly typed structures and classes

    ReplyDelete
  4. Roel,
    according to error message you used the same PublicKeyToken as in my assembly. If you compiled NVelocity by yourself - you need to use your own key file for strong name generation. In this case PublicKeyToken will be different. So replace all occuriences in the code of "407dd0808d44fbdc" by your PublicKeyToken.

    ReplyDelete
  5. Hi!
    Thank you for this great post! I was fighting with this same issue and your instructions helped me to get it work.

    Thank you very much!

    ReplyDelete
  6. There is a simpler, non-intrusive way of using NVelocity from GAC, which does not require original NVelocity assembly recompilation. The problem is that the type resolver is not able to match an unqualified "NVelocity" name to fully qualified name from GAC. But we can match them manually. To do this execute following code before the first use of NVelocity:

    AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
    {
    var name = new AssemblyName(eventArgs.Name).Name;
    return AppDomain.CurrentDomain.GetAssemblies()
    .Where(a => String.Compare(a.GetName().Name, name, true) == 0)
    .FirstOrDefault();
    };

    ReplyDelete
  7. hi ksuszka,
    good solution - thanks for sharing it here. I would add here additional check in order to use this code only for NVelocity, while other assemblies should still be resolved using full name.
    This configuration can be done in global.asax, however in case of Sharepoint modifying of global.asax is not common and required technique. Http module is also option (with some extra code).

    ReplyDelete