Tuesday, September 2, 2008

Sling OSGi Track pt 5: Bundling initial content

This is the continuation of

In this part, I will show how to bundle initial content.

I will put the test node and the test script that I used previously into the bundle, so they don't need to be created separately.

Sling provides several different ways to load content provided by an OSGi bundle. These are provided in the org.apache.sling.jcr.contentloader package, and can be used to

  • create initial content from a filesystem structure contained in a ZIP or JAR file
    This content reader will create nt:folder and nt:file resources. It basically does the same as if you created the same items through WebDAV.
    It also has the same limitations: you cannot set different Nodetypes, and you cannot edit properties explicitly.
  • create initial content from an XML definition
    This allows you to provide an XML file, in which the content structure is defined. You can define nodetypes and mixins, but you cannot influence any other properties.
    As opposed to the ZIP reader, it does not provide a way to include binary data.
  • create initial content from a JSON definition 
    This is the most powerful reader, as you can set arbitrary properties (but again, no binary content can be included).

Aims:

  • Show how to bundle initial sling content into the OSGi bundle

Ingredients:

Files:

Outline:

  • Project structure
  • Create initial content structure
  • Update the pom accordingly
  • Build and Install
  • Test

Execution:

Project structure
.
|-- pom.xml
`-- src
    `-- main
        |-- java
        |   `-- mh
        |       `-- osgitest
        |           |-- SampleService.java
        |           `-- SampleServiceImpl.java
        `-- resources
            |-- META-INF
            `-- SLING-INF
                `-- initial-content
                    |-- apps
                    |   `-- samples
                    |       `-- osgitest
                    |           `-- GET.esp
                    `-- content.json

Create initial content structure

We'll be using the JAR content loader to create the structure apps/samples/osgitest and the file GET.esp beneath it.
For that, we create a directory src/main/resources/SLING-INF/initial-content. In that directory, we'll create the directory apps/samples/osgitest.
Into that directory, we place the file GET.esp with the following contents:

  1: <%
  2: var service =
  3:  sling.getService(Packages.mh.osgitest.SampleService);
  4: %><%= service.sayHello() %>

That was easy so far: all we wanted to do was create resources of type nt:folder and nt:file.
(Note that the changes to the pom are needed before sling takes notice, so don't try right now).

For creating the structure in the content tree, it's not that easy. If you peek back at the first part, we need to create a resource of type "nt:unstructured" and assign it a property of "sling:resourceType" with a value of "samples:osgitest", so that the GET.esp script at apps/samples/osgitest is picked up.

Also, I now want to create the script at /content/osgitest/test, instead of /content/test, so this does not clutter up the content tree at root level.

This can only be achieved through the JSON reader, which expects a JSON definition of the nodes to be created. It's actually very intuitive and need little explanation.
In the initial-content/ directory, I place a file named "content.json" with the following content:

  1: {
  2:     "jcr:primaryType" : "nt:unstructured",
  3:     "osgitest" : {
  4:         "jcr:primaryType" : "nt:unstructured",
  5:         "test" : {
  6:             "jcr:primaryType" : "nt:unstructured",
  7:             "sling:resourceType" : "samples:osgitest"
  8:         }
  9:     }
 10: }

This will create the following structure in the content repository

snap001 2008-09-02

Update the pom accordingly

The contentloader service inspects the Manifest for the "Sling-Initial-Content" header.
To create this header, we add line 13 below to the pom.

The header can contain multiple entries, separated by commas, and can contain directives whether content should be overwritten, whether it should be deleted when the bundle is uninstalled, and what path other then the root node the content should go to. All this is described at http://incubator.apache.org/sling/site/content-loading.html, and in even more detail at
http://svn.apache.org/viewvc/incubator/sling/trunk/jcr/contentloader/src/main/java/org/apache/sling/jcr/contentloader/internal/PathEntry.java?view=markup and
http://svn.apache.org/viewvc/incubator/sling/trunk/jcr/contentloader/src/main/java/org/apache/sling/jcr/contentloader/internal/Loader.java?view=markup.

The "overwrite" part is dangerous: it will first delete the whole path and then add your contents, so in our case above, the folders "content" and "apps" would be empty except  for the content we just filled in. Ouch.
This can be avoided by simultaneously restricting the deployment to a path. But note that this does not work in CRX (not the cup edition, at least).

But take care you understand that by default, existing content is NOT overwritten, so if you re-deploy your package and want to see the effect of your changes, you'll need to delete everything that must be replaced before deployment -- it won't be overwritten.

  1: <plugin>
  2: 	<groupId>org.apache.felix</groupId>
  3: 	<artifactId>maven-bundle-plugin</artifactId>
  4: 	<version>1.4.3</version>
  5: 	<extensions>true</extensions>
  6: 	<configuration>
  7: 		<instructions>
  8: 			<Export-Package>mh.osgitest</Export-Package>
  9: 			<Import-Package>org.osgi.framework;version="1.3.0"</Import-Package>
 10: 			<Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
 11: 			<Bundle-Name>${pom.name}</Bundle-Name>
 12: 			<Bundle-Vendor>Moritz Havelock</Bundle-Vendor>
 13: 			<Sling-Initial-Content>SLING-INF/initial-content</Sling-Initial-Content>
 14: 		</instructions>
 15: 	</configuration>
 16: </plugin>
Build and Install

Before we build and install, remove the old test artifacts from the repository (the apps/samples/osgitest folder and the /content/test script).

Then, as before, just do "mvn clean install".

Test

We've moved the test script a bit, so now go to http://localhost:7402/osgitest/test to see our service say "hello".

Properly done

Above I mentioned that a combination of path specifications, overwrite and uninstall values is possible to enable the behaviour that is probably what someone would intuitively expect as the default behaviour.
So, I want to

  • deploy content from the package
  • if resources are already existing where I want to deploy a resource, overwrite it
  • leave other resources alone

For that, I need to attach content-definitions to specific paths. For that, I need to re-structure my initial-content directory a bit:

src/main/resources/SLING-INF/
`-- initial-content
    |-- apps
    |   `-- samples
    |       `-- osgitest
    |           `-- GET.esp
    `-- content
        `-- osgitest
            `-- test.json

Then, I change my Sling-Initial-Content tag in the pom to look like this:

  1: <Sling-Initial-Content>						
  2:   SLING-INF/initial-content/content/osgitest;path:=/content/osgitest;overwrite:=true;uninstall:=false,
  3:   SLING-INF/initial-content/apps/samples/osgitest;path:=/apps/samples/osgitest;overwrite:=true;uninstall:=true
  4: </Sling-Initial-Content>

And the file test.json looks like this:

  1: {
  2:     "jcr:primaryType" : "nt:unstructured",
  3:     "sling:resourceType" : "samples:osgitest"
  4: }

What does all that do?

  • In the first line in the Sling-Initial-Content tag in the pom, I state that I want to have content created at /content/osgitest. This restricts the whole operation to that path and makes sure that no other resources are touched (i.e. deleted in the overwrite). But that also means that my content definition in the JSON file must no longer contain the parent nodes of the "test" node. That is why the file now has a new name and contains only the properties of the single node it defines.
    By saying that uninstall is false, I make sure that when the package is uninstalled, the content still remains. You might want this for your package or no.
    Do note that even in this scenario, the whole subtree of /content/osgitest will be deleted before the content is re-deployed. It's just everything outside of that path that is unaffected.
  • The second line does basically the same, but points deeper into the path through the path directive. Contents will be uninstalled when the bundle is uninstalled.
    I could have omitted the uninstall directive here, as it defaults to the value of the overwrite directive.

No comments: