CMIS mit Apache Chemistry – ein Praxisbeispiel

Die Content Management Interoperability Services können einen breiten Bereich von Einsatzszenarien im Bereich des Enterprise Content Management (ECM) abdecken. Eine beispielhafte Implementierung einer Client-Applikation oder eines CMIS-Servers mit Apache Chemistry.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 19 Min.
Von
  • Jens Hübel
Inhaltsverzeichnis

Die Content Management Interoperability Services können durch ihre Plattformneutralität und Sprachenunabhängigkeit einen breiten Bereich von Einsatzszenarien im Bereich des Enterprise Content Management (ECM) abdecken. Der Artikel zeigt beispielhaft, wie sich mit Apache Chemistry eine CMIS-Bibliothek nutzen lässt, um eine Client-Applikation oder einen CMIS-Server zu implementieren.

Etwas wie SQL im Bereich relationaler Datenbanken fehlte bisher für Content-zentrierte Daten. CMIS (Content Management Interoperability Services) hat den Anspruch, in diese Lücke zu springen und eine gemeinsame Sprache zu definieren, die unterschiedliche Systeme implementieren können, die mit Content umgehen.

Für einfache Funktionen reicht ein Browser als Client-Applikation für CMIS (Content Management Interoperability Services) aus. Ist kein CMIS-fähiges Repository vorhanden, kann der Entwickler eine der zwei Beispielimplementierungen von Apache Chemistry, einem neuen Top-Level-Projekt der Open-Source-Organisation, verwenden (fileshare oder in-memory), die sich mit wenigen Handgriffen einrichten lassen. Hierzu benötigt er Java, einen Servlet-Container wie Jetty oder Tomcat und die .war-Dateien von OpenCMIS.

Sollte man noch kein JDK installiert haben, ist als Erstes Oracles Java SDK herunterzuladen und zu installieren (die Java-Runtime genügt nicht für Tomcat). Als Nächstes besorgt man sich den Tomcat 6 und die In-Memory-Webapplikation von OpenCMIS. Nach dem Entpacken des Tomcat-Archivs öffnet der Entwickler unter Windows einen Command-Prompt und wechselt ins bin-Verzeichnis des Tomcat. Dort befindet sich ein Startskript (startup.bat für Windows, startup.sh für Unix). Nach dessen Aufruf sollte man einen laufenden Webserver haben. Tippt man nun in einem Webbrowser die Adresse http://localhost:8080/ ein und sieht die Tomcat-Startseite, war das Vorgehen erfolgreich. Kommt es zu Fehlern, liegt das häufig daran, dass die Umgebungsvariable JAVA_HOME nicht oder nicht richtig gesetzt ist. Sie muss auf das Verzeichnis zeigen, in dem das JDK installiert ist.

Als Nächstes öffnet der Entwickler das Zip-Archiv von OpenCMIS und extrahiert die Datei chemistry-opencmis-server-inmemory-0.2.0-incubating.war. Nicht notwendig, aber sinnvoll, um viel Tipparbeit zu sparen, ist es, diese Datei in einen kurzen Namen umzubenennen, zum Beispiel in opencmis.war. Dann kopiert man die Datei ins webapp-Verzeichnis der Tomcat-Installation. Der Servlet-Container sollte die neue Webapplikation automatisch erkennen und nach einigen Sekunden ein Verzeichnis opencmis innerhalb von webapps erstellen. In der mitgelieferten Standardkonfiguration legt der In-Memory-Server ein Repository mit der ID "A1" an sowie einige Typen, Ordner und Dokumente.

Läuft der Server, kann der Entwickler sich mit einem Webbrowser verbinden und sich die Repository-Information anzeigen lassen. Dazu tippt er http://localhost:8080/opencmis/atom in die Adressenzeile ein. Der Browser erhält eine Antwort, deren Dateityp er jedoch nicht kennt und die er in eine Datei speichern sollte. Je nach Browser hat diese unterschiedliche Namen, und man benennt sie am besten um, etwa in repoInfo.xml. Zieht der Programmierer die Datei per Drag & Drop auf den Browser, kann er sich die Antwort des Servers in Form einer AtomPub-Antwort ansehen. Zu erkennen ist, dass der Beispielserver ein einziges Repository enthält, das den Namen "A1" trägt. Nach dem Expandieren des RepositoryInfo-Tags erhält man genauere Informationen darüber.

Danach sei der Inhalt des Root-Folders aufgelistet. Diesen können die meisten Browser sogar direkt anzeigen, allerdings sieht der Entwickler nur einen Teil der gelieferten Information. Den Link dazu findet er unter dem Tag "app:collection" mit dem collectionType "root" (der Root-Folder des Repository hat die ID 100):

http://localhost:8080/opencmis/atom/A1/children?id=100

Darauf antwortet der Server mit einem Feed im AtomPub-Format, den der Browser anzeigen kann.

Der Root-Folder enthält drei Dokumente (My_Document-0-0, My_Document-0-1 und My_Document-0-2) sowie zwei Ordner (My_Folder_0_0 und My_Folder-0-1). Viele Informationen sind allerdings in den CMIS-spezifischen Erweiterungen des AtomPub-Protokolls enthalten, die der Browser aber nicht kennt und deshalb ignoriert. Sie lassen sich anzeigen, wenn man den Quelltext des Feeds anschaut (Ansicht | Seitenquelltext anzeigen).

Feed im AtomPub-Format, dargestellt mit dem Firefox-Browser (Abb. 1)

Hier erkennt man zum Beispiel einen Link auf den Inhalt des Dokuments:

<atom:content src="http://localhost:8080/opencmis/atom/A1/content/
data.txt?id=133" type="text/plain"/>

oder die Properties des Dokuments in <cmis:properties>.

Der In-Memory-Server gestattet nicht nur lesenden Zugriff, sondern es lassen sich auch Dokumente und Ordner erstellen oder modifizieren. Hierzu reicht der Webbrowser allerdings nicht aus, da dazu HTTP-POST- und -PUT-Requests zu senden sind. Möchte man das Protokoll im Detail verstehen, bieten sich dazu Tools wie wget oder curl an. Zudem unterstützt der In-Memory-Server das SOAP-Binding. Die WSDL (Web Services Description Language) des Repository-Services kann man mit folgender URL im Browser anzeigen.

http://localhost:8080/opencmis/services/RepositoryService?wsdl

Lässt der Entwickler den Parameter ?wsdl weg, bekommt er eine Übersichtsseite mit allen Services und deren URLs.

Der Programmierer wird jedoch schnell feststellen, dass eine Implementierung auf Protokollebene ziemlich aufwendig ist. Eine Bibliothek, die eine komfortable API in der jeweiligen Zielsprache anbietet, kann einem dabei viel Arbeit ersparen. Das war die Motivation hinter dem Apache-Chemistry-Projekt, das eine solche API für Java (opencmis) und einige weitere Sprachen anbietet. Wird direkt auf Protokollebene entwickelt, kommt man mit dem SOAP-Binding schneller zum Ziel als mit AtomPub, sofern die Entwicklungsplattform den WSDL-Import unterstützt.

Für Endanwender ist es jedoch wesentlich komfortabler, einen CMIS-spezifischen Client zu verwenden, der das Protokoll vollständig versteht. Einen solchen bietet das Apache-Projekt mit der CMIS-Workbench.

Das Apache-Projekt kann die Arbeit einer CMIS-Integration erheblich erleichtern, sowohl auf Client- als auch auf Serverseite. Chemistry besteht aus mehreren Teilprojekten. Client-Bibliotheken gibt es für Java, PHP, Python und .NET, ein Server- und ein Test-Framework nur für Java. Das Teilprojekt, das sich mit der Java-Implementierung befasst, trägt ebenfalls den Namen opencmis. Es ist das funktional umfassendste Teilprojekt und liegt in einem zweiten Incubator-Release (0.2.0) vor. Das erste TLP-Release 0.3.0 (Top-level Project) wird in Kürze erwartet. Python hat sein erstes Release (0.4.1) bereits erreicht. Die anderen Teilprojekte sind noch in der Entwicklungsphase.

Chemistry entstand ursprünglich als Ableger aus dem Jackrabbit-Projekt, vornehmlich als Initiative von Nuxeo und unterstützt von Day. Schon bald kamen unabhängig von Jackrabbit Entwickler von Alfresco, Open Text und SAP mit dem Ziel zusammen, gemeinsam eine Client-Bibliothek für Java zu entwickeln. Schnell wurde klar, dass es sinnvoll ist, beide Projekte zu vereinigen. Jackrabbit läuft aber unabhängig von Chemistry als eigenständiges Apache-Projekt weiter. Inzwischen ist Chemistry weiter gewachsen, und es unterstützt zusätzlich PHP, Python und .NET. Seit Ende Februar hat es den Incubator-Status verlassen und ist nun ein Top-Level-Projekt.

Die Client-Schnittstelle gliedert sich in zwei Schichten. Die opencmis-Bindings (Java-Package org.apache.chemistry.opencmis.client.bindings) abstrahieren von den verwendeten Protokollen (AtomPub und SOAP) und setzen das CMIS-Modell auf Java-Klassen um. Ein Entwickler gibt beim Verbindungsaufbau an, welches Protokoll und welche Verbindungsparameter zu verwenden sind (URL zum Server, Benutzerkennung und Passwort). Diese Binding-Klassen sind reine Datenschnittstellen. Sie kapseln zwar das Protokoll ab, folgen aber nicht objektorientierten Prinzipien.

Ferner werden Entwickler für reale Anwendungen schnell bestimmte Komfortfunktionen vermissen. Häufig benötigen sie beispielsweise die Typinformationen, um IDs auf lesbare Namen umzusetzen oder um festzustellen, welchen Datentyp eine bestimmte Property besitzt. Ein clientseitiges Cachen der Typinformationen ist daher wünschenswert. Oft möchte man auch nicht nach jeder kleinen Änderung Daten zum Server senden, sondern mehrere Aufrufe aufsammeln und dann in einem Rutsch abschicken. Die CMIS-Client-API (org.apache.chemistry.opencmis.client.api) leistet das und bietet eine höhere Ebene mit CMIS-Objekten und -Methoden. Intern setzt sie auf das Client-Binding auf. Die meisten Anwendungen sollten die Client-API zur Integration verwenden. Ein kleiner Codeauschnitt kann einen Eindruck geben, wie die API zu verwenden ist. Wer tiefer einsteigen will, sei auf die Projekt-Website verwiesen. Auch die CMIS-Workbench verwendet diese API.

Ein Wort noch zur Programmierumgebung. Am schnellsten lässt sich eine Umgebung mit Apache Maven aufsetzen. Das Build-System wird auch intern zur Produktion von opencmis verwendet. Der Entwickler startet lediglich mit einer Konfigurationsdatei, Maven sorgt dann für den Download aller benötigten Bibliotheken aus öffentlichen Repositories und kann die passende Umgebung für Entwicklungswerkzeuge wie Eclipse erzeugen. Maven ist aber keine Voraussetzung. Der Programmierer kann auch mit Eclipse anfangen und sich manuell seinen Workspace einrichten oder andere Build-Werkzeuge verwenden. Das Package opencmis-ful-xxx.zip enthält alle benötigten Bibliotheken, die in ein Projekt einzubinden sind.

Der erste Schritt in einem CMIS-Client besteht immer im Aufbau der Verbindung zum Server. Dazu bedient man sich der Session-Klasse:

import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.SessionParameter;
...
SessionFactory f = SessionFactoryImpl.newInstance();
Map<String, String> parameter = new HashMap<String, String>();

// user credentials (not needed for in-memory)
parameter.put(SessionParameter.USER, "ix");
parameter.put(SessionParameter.PASSWORD, "ix");

// connection settings
// AtomPub
parameter.put(SessionParameter.BINDING_TYPE,
BindingType.ATOMPUB.value());
parameter.put(SessionParameter.REPOSITORY_ID, "A1");
parameter.put(SessionParameter.ATOMPUB_URL,
"http://localhost:8080/inmemory/atom");

Im Parameter-Objekt gibt der Entwickler Verbindungsinformationen wie Serveradresse, Benutzer und Passwort an. Für Webservices benötigt er die Adresse jedes einzelnen Web-Services.

     parameter.put(SessionParameter.BINDING_TYPE, 
BindingType.WEBSERVICES.value());
parameter.put(SessionParameter.REPOSITORY_ID, "A1");
parameter.put(SessionParameter.WEBSERVICES_ACL_SERVICE,
"http://localhost:8080/inmemory/services/ACLService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_DISCOVERY_SERVICE,
"http://localhost:8080/inmemory/services/DiscoveryService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_MULTIFILING_SERVICE,
"http://localhost:8080/inmemory/services/MultiFilingService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_NAVIGATION_SERVICE,
"http://localhost:8080/inmemory/services/NavigationService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_OBJECT_SERVICE,
"http://localhost:8080/inmemory/services/ObjectService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_POLICY_SERVICE,
"http://localhost:8080/inmemory/services/PolicyService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_RELATIONSHIP_SERVICE,
"http://localhost:8080/inmemory/services/RelationshipService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_REPOSITORY_SERVICE,
"http://localhost:8080/inmemory/services/RepositoryService?wsdl");
parameter.put(SessionParameter.WEBSERVICES_VERSIONING_SERVICE,
"http://localhost:8080/inmemory/services/VersioningService?wsdl");

Der eigentliche Verbindungsaufbau erfolgt dann mit:

session = f.createSession(parameter);

Mit der Session hat der Programmierer Zugriff auf die weiteren Funktionen des Servers. Er kann sich etwa die RepositoryInfo holen und anzeigen lassen:

     RepositoryInfo repoInfo = session.getRepositoryInfo();
System.out.println("Repository Name: " + repoInfo.getName());
System.out.println("Description: " + repoInfo.getDescription());
// ...

oder sich den Inhalt des Root-Folders auflisten lassen:

     Folder rootFolder = session.getRootFolder();
ItemIterable<CmisObject> children = rootFolder.getChildren();
System.out.println("Name: " + obj.getName());
System.out.println("Id: " + obj.getId());
System.out.println("Basistyp: " + obj.getBaseType().getDisplayName());
// ...

Nach dem gleichen Muster kann man weiter verfahren, um den Content eines Dokuments zu holen, Queries abzusetzen oder Ähnliches. Die CMIS-Workbench stellt eine Fundgrube an Beispielcode dar, die zur Orientierung für eigene Zwecke helfen kann.

Auch wenn mehr Applikationsentwickler die Notwendigkeit in einer Client-Bibliothek sehen dürften, skizziert der Artikel grob auch eine Serverimplementierung. Mit dem Server-Framework aus opencmis ist das gar nicht so schwierig. Im Prinzip bringt die Java-Bibliothek bereits den ganzen Server mit. Sie enthält eine Servlet-Klasse und die Konfigurationsdatei web.xml. Entweder kompiliert man diese selbst mit Maven, oder man lädt sich von der Chemistry-Distribution das Paket chemistry-opencmis-dist-0.2.0-incubating-server.zip herunter. Dieses enthält eine fertige Webapplikation (.war), die allerdings erst mal nichts unternimmt. Ein Programmierer eines Servers muss erst für sinnvolle Funktionen sorgen. Im Wesentlichen besteht das aus der Implementierung der Java-Schnittstelle CMISService. Neben der .war-Datei liegt in dem Paket noch die Bibliothek chemistry-opencmis-server-support-0.2.0-incubating.jar. Diese benötigt man beim Implementieren eines Query-Parsers und kopiert sie dann gegebenenfalls ins lib-Verzeichnis der Webapplikation.

Bekanntermaßen ist eine .war-Datei nichts anderes als ein Zip-Archiv. Als Ausgangspunkt entpackt man dieses und nimmt die enthaltene Ordnerstruktur als Vorbild für die eigene Serverimplementierung. Alle benötigten .jar-Bibliotheken für einen Server befinden sich daher im Unterverzeichnis WEB-INF/lib. Die eigene Implementierung der Schnittstelle CMISService (auf die noch eingegangen sei) muss mit dem Server-Framework in Verbindung treten. Dazu gibt es eine Datei repository.properties, die man im WEB-INF/classes-verzeichnis findet und für das man als Beispiel folgenden Eintrag bekommt:

class=de.heise.cmisserver.HeiseServiceFactoryImpl

Diese Factory-Klasse liefert unter anderem die Implementierung von CMISService und sieht wie folgt aus:

public class HeiseServiceFactoryImpl extends AbstractServiceFactory {

@Override
public void init(Map<String, String> parameters) {
}

@Override
public CmisService getService(CallContext context) {
return new HeiseServiceImpl();
}

@Override
public void destroy() {
}
}

Die init()- und destroy()-Methoden lassen sich für Initialisierungscode beim Starten des Servers und bei Aufräumarbeiten am Ende benutzen. Die entscheidende Methode ist getService(), die eine Instanz der eigentlichen Serverimplementierung zurückgibt. Für das Beispiel ist als einzige Methode getRepositoryInfo() zu implementieren:

public class HeiseServiceImpl extends AbstractCmisService {

@Override
public List<RepositoryInfo> getRepositoryInfos(ExtensionsData arg0) {

RepositoryInfoImpl repoInfo = new RepositoryInfoImpl();
String rootFolderId = „1000“;
repoInfo = new RepositoryInfoImpl();

// set capabilities
RepositoryCapabilitiesImpl caps
= new RepositoryCapabilitiesImpl();
caps.setAllVersionsSearchable(false);
caps.setCapabilityAcl(CapabilityAcl.NONE);
caps.setCapabilityChanges(CapabilityChanges.NONE);
caps.setCapabilityContentStreamUpdates(
CapabilityContentStreamUpdates.NONE);
caps.setCapabilityJoin(CapabilityJoin.NONE);
caps.setCapabilityQuery(CapabilityQuery.NONE);
caps.setCapabilityRendition(CapabilityRenditions.NONE);
caps.setIsPwcSearchable(false);
caps.setIsPwcUpdatable(false);
caps.setSupportsGetDescendants(true);
caps.setSupportsGetFolderTree(true);
caps.setSupportsMultifiling(false);
caps.setSupportsUnfiling(false);
caps.setSupportsVersionSpecificFiling(false);

repoInfo.setId("heise");
repoInfo.setName("Heise-Repository");
repoInfo.setDescription("Heise Developer CMIS Demo");
repoInfo.setCmisVersionSupported("1.0");
repoInfo.setCapabilities(caps);
repoInfo.setRootFolder(rootFolderId);
repoInfo.setPrincipalAnonymous("anonymous");
repoInfo.setPrincipalAnyone("anyone");
repoInfo.setThinClientUri(null);
repoInfo.setChangesIncomplete(Boolean.TRUE);
repoInfo.setChangesOnType(null);
repoInfo.setLatestChangeLogToken(null);
repoInfo.setVendorName("Heise Demo");
repoInfo.setProductName("Heise-Developer Demo-Server");
repoInfo.setProductVersion("1.0");
repoInfo.setAclCapabilities(null);

List<RepositoryInfo> repoInfoList = new
ArrayList<RepositoryInfo>();
repoInfoList.add(repoInfo);
return repoInfoList;
}
...
}

In der gleichen Weise verfährt man analog mit den anderen abstrakten Methoden aus AbstractCmisService. Es empfiehlt sich in der Regel, von der Klasse AbstractCmisService abzuleiten, statt direkt das Interface CmisService zu implementieren, denn das spart Arbeit: Für das AtomPub-Protokoll ist etwas mehr Information notwendig, als die Rückgabewerte der einzelnen Methoden aus CmisService zu liefern, um einen vollständigen Atom-Feed oder ein Atom-Entry erzeugen zu können. Die Klasse AbstractCmisService bietet dafür bereits eine Standardimplementierung an, sodass der Entwickler sich darum nicht kümmern muss. (Für produktive Server sei jedoch empfohlen, die Methode getObjectInfo() selber zu implementieren, da das unter Umständen deutlich effizienter als die Standardimplementierung.)

Die restlichen Methoden des CMISService sind reine Fleißarbeit und erfolgen nach dem gleichen Muster. Heraus kommt ein lauffähiger CMIS-Server, der eine einzige Methode sinnvoll beantwortet. Man kann jetzt aus seiner IDE heraus oder dem Build-Werkzeug seiner Wahl eine Webapplikation (cmis-server.war) bauen und diese in das webapp-Verzeichnis des Tomcat kopieren. Für Eclipse steht im Download-Bereich ein vollständiges Projekt (huebel_cmis_eclipse-workspace.zip) bereit. Bei Benutzung der "Eclipse IDE for Java EE Developers" kann man sein Projekt einfach auf einen im Eclipse konfigurierten Webserver (zum Beispiel Tomcat) ziehen. Dadurch nutzt der Entwickler einfach den Debugger, wenn er genauer verstehen möchte, wie der eigene Code aufgerufen wird oder was intern in opencmis läuft.

Hat der Programmierer das .war-Archiv installiert, kann er den Server aufrufen. Der erste Test erfolgt am besten aus dem Webbrowser, in dem man folgende Adresse eingibt:

http://localhost:8080/cmis-server/atom

Der Server antwortet mit einer XML-Datei, die man sich in einem Texteditor ansehen kann (siehe oben). Der Service versteht jedoch auch das SOAP-Binding: Mit

http://localhost:8080/cmis-server/services/RepositoryService?wsdl

bekommt der Entwickler eine WSDL-Datei, die er weiterverarbeiten kann (ersetzt er "RepositoryService" durch die anderen Servicenamen, funktioniert das entsprechend). Ohne die Endung ?wsdl sieht der Programmierer eine HTML-Seite mit allen Webservices. Probiert er die CMIS-Workbench aus, kann er sich immerhin verbinden.

Die CMIS-Workbench kann sich mit dem Beispielserver verbinden (Abb. 2).

Versucht man nun den Login, lädt die Workbench im nächsten Schritt das Typsystem – und mangels Implementierung schlägt das fehl. Möchte der Entwickler mehr Funktionen haben, orientiert er
sich am besten an den zwei Beispielservern in Chemistry: in-memory und fileshare. Vorzugsweise beginnt er mit einem minimalen Typsystem (zunächst reichen cmis:document und cmis:folder), dann implementiert er getChildren im NavigationService, was zumindest bei Übergabe der ID des Root-Folders eine sinnvolle Antwort geben sollte. Von dort kann man sich Stück für Stück weiterhangeln.

Erwähnt sei noch, dass ein Parser für die CMIS-Query-Sprache ziemlich aufwendig zu implementieren ist. Das Server-Framework bietet dazu Hilfe in Form eines integrierten Parsers an, sodass man sich für viele Fälle auf eine relativ einfache Umsetzung auf die spezielle Query-Sprache eines Repository oder direkt auf SQL beschränken kann. Näheres dazu ist auf den Chemistry-Webseiten zu finden.

Chemistry ermöglicht es, mit wenig Aufwand zu einem funktionsfähigen System mit Client und Server zu kommen. Es bietet jedoch weit mehr, als der Artikel schildern kann. Nichts steht darüber hinaus Experimenten mit Python, Groovy oder .NET im Weg. In jedem Fall lohnt es sich, die Projektwebseiten zu besuchen, die in den nächsten Wochen weiter ausgebaut und vervollständigt werden. Auch die Mailing-Liste bietet sich für weitere Fragen an.

Jens Hübel
ist bei OASIS im CMIS Technical Committee Vertreter für die Open Text Software, für die er als Softwarearchitekt in München arbeitet. Außerdem ist er Committer bei der Apache Software Foundation. Er beschäftigt sich seit vielen Jahren beruflich mit Enterprise Content Management.

  1. Jens Hübel; An einem Strang; CMIS: Eine Sprache für das Content Management; in iX 3/2011, S. 128
  2. Christian Thiede; Glücksgriff; CMIS – neuer Standard für die ECM-Industrie; Artikel auf heise Developer

(ane)