Auch bei der Pluginentwicklung für Confluence kommt irgendwann der Zeitpunkt, an dem man sich mit komplexeren Abläufen bei der Persistierung von Daten beschäftigen muss. Um die Integrität dieser Daten sicherstellen zu können, ist man dann auf den Einsatz von Transactions angewiesen. Da Confluence das Spring Framework verwendet, bietet sich hierfür die Nutzung des TransactionTemplate an. Dieses ermöglicht es, wie in der Spring Dokumentation beschrieben, auf einfache Weise kritischen Code unter Verwendung eines Callbacks in eine Transaction zu verpacken. Dazu benötigt man jedoch noch einen PlatformTransactionManager. Dieser implementiert die Strategie für die Transaktionsbehandlung, die im Fall von Confluence auf Hibernate basiert. Auch hierzu können weitere Informationen und ein Codebeispiel auf den Seiten von Spring gefunden werden. Der PlatformTransactionManager ist als Bean im Application Context von Confluence vorhanden und kann über den ComponentContainer referenziert werden:
public PlatformTransactionManager getTransactionManager(){
return (PlatformTransactionManager) ComponentContainer.get("transactionManager");
}
Allerdings funktioniert dieses Codebeispiel nach einer Umstellung auf V2-Plugins nicht mehr. Der Grund: Da diese Plugins in OSGI-Bundles umgewandelt werden, welche eine andere Spring Version verwenden als die Kern-Applikation, kommt es bei der Referenzierung der transactionManager-Bean über den ComponentContainer zu einer ClassCastException.
Bei der Suche nach einer Lösung für dieses Problem bin ich über ein Ticket im Issue Tracker von Atlassian gestolpert, in dessen Kommentaren eine Alternative beschrieben wird: Abhilfe schafft die Verwendung der Shared Access Layer (SAL), einer einheitlichen Service-Schicht für alle Atlassian Anwendungen. Sie stellt neben verschieden anderen Services auch ein TransactionTemplate bereit, das für Confluence bereits den auf Hibernate basierenden PlatformTransactionManager gesetzt hat und auf folgende Weise referenziert werden kann.
Zunächst muss das Template als Komponente (Spring-Bean) im Plugin-Descriptor (atlassian-plugin.xml) importiert werden:
<component-import name="SAL Transaction Template" key="salTransactionTemplate">
<interface>com.atlassian.sal.api.transaction.TransactionTemplate</interface>
</component-import>
Dann kann man sich die Komponente per Spring Autowiring in fast alle Plugin Modultypen (z. B. Actions und Components ) injizieren lassen. Der Name der zu injizierenden Bean entspricht dem Key des component-import Elementes im Plugin-Descriptor (in diesem Beispiel also “salTransactionTemplate” ):
public void setSalTransactionTemplate(TransactionTemplate template){
this.transactionTemplate = template;
}
Nun kann das Template wie oben beschrieben verwendet werden.
Noch ein Hinweis: SAL wird erst ab Confluence 3.0 mit ausgeliefert. Für Confluence 2.10 muss man also eine andere Lösung finden.
<
Michael Hummel, Geschäftsführer der empulse GmbH, stellte in seinem Vortrag beim Confluence Community Day vor, wie man mit Hilfe von Confluence ein Software-Handbuch realisieren kann. Die Vortragsfolien können bei Slideshare gefunden werden:
Die von Herrn Hummel präsentierte Lösung ersetzt das gedruckte Handbuch einer Software für Reiseunternehmen. Dieses litt an geringer Akzeptanz bei den Kunden, die somit auch für triviale Fragen den in der Folge überlasteten Support in Anspruch nahmen.
Aus dieser Ausgangssituation leiteten sich die Anforderungen an ein neues Handbuch ab: Die Akzeptanz bei den Kunden sollte gesteigert und somit der Support entlastet werden. Außerdem sollten die Kosten für den Druck des Handbuches gesenkt, sowie die Entwickler der Software von den aufwendigen Reviews des Handbuches für jedes neue Release befreit werden.
Diese Anforderungen wurden mittels Atlassian Confluence realisiert. Dazu wurde das Confluence Wiki von emplse erweitert und ein spezielles Inhaltskonzept entwickelt. Durch eine entsprechende Strukturierung des Inhaltes in Bereiche und Seitenhierarchien konnten die Formularstruktur der Software sowie Mehrsprachigkeit und kundenspezifische Inhalte (z. B. Arbeitsanweisungen) auf das Wiki abgebildet werden. Ein weiterer Vorteil ergibt sich für die Kunden in der direkten Integration der Hilfe in die Software, die mittels eines von empulse entwickelten Webservice auch nach verwandten Hilfethemen zu einem Formularelement sucht. Dazu wird zu jedem Formularelement in der Software eine Seite im Wiki gepflegt, die sich bei Aufruf der Hilfe im Webbrowser öffnet. Des Weiteren profitieren Support und Kunden von der kollaborativen Idee eines Wikis: Supportmitarbeiter können, anfangs geführt durch einen Moderator, selbstständig Änderungen an unklaren Formulierungen in der Hilfe vornehmen. Die Hinweise dazu können Kunden durch Verwendung der Kommentarfunktion in Confluence geben. Dies führt zu einer ständigen Verbesserung des Handbuches.
Weitere Informationen zum Projekt können auch im Blog von empulse gefunden werden.
Seit Confluence 2.10 gibt es einen ziemlich unangenehmen Bug im Trigger-Plugin-Modultyp:
Manchmal kommt es vor, dass ein Plugin bei der Installation nicht erfolgreich aktiviert werden kann. In diesem Fall werden alle Module deaktiviert, also auch der Trigger. Dabei wird aber auch versucht den zugehörigen Job zu deaktivieren. Ist der zu dem Zeitpunkt noch nicht vollständig geladen, kommt es zu einer IllegalStateException. In der Folge befindet sich das Plugin in einer Art “Zwischenzustand”. Es erscheint zwar als installiert und aktiviert in der Pluginübersicht, aber keine der vom Plugin bereitgestellt Funktionen sind verfügbar. Außerdem kann das Plugin erst nach einem Neustart von Confluence aus dem System entfernt werden. Jeder Versuch es ohne Neustart zu deinstallieren wird mit einer org.springframework.orm.ObjectRetrievalFailureException quittiert.
Da dieses Problem die Wartung von Kundeninstallationen und Nightly Builds erheblich erschwert, musste ein Workaround her. Da es seit Confluence 2.10 die Möglichkeit gibt eigene Plugin Modultypen zu definieren indem man den neuen Module-Type-Modultyp verwendet, war es relativ einfach eine Lösung zu finden, die sogar als Plugin installiert werden kann (keine Anpassung des Confluence Kerns nötig!): Man definiert einen Modultyp, der von TriggerModuleDescriptor (der Implemetation des Trigger-Plugin-Modultyps) erbt. Dort überschreibt man die Methode disabled(), deren Aufruf die Exception verursacht:
public class RobustTriggerModuleDescriptor extends TriggerModuleDescriptor {
/**
* Logger for this class.
*/
static Logger LOG = Logger.getLogger(RobustTriggerModuleDescriptor.class);
static final String WARN_MESSAGE = "Using workaround for CONF-14828. "+
"Code might be deprecated.";
static final String ERROR_MESSAGE = "Disabling of module failed.";
public void init(final Plugin plugin, final Element element)
throws PluginParseException {
Scheduler sched = (Scheduler) ContainerManager.getComponent("scheduler");
setScheduler(sched);
super.init(plugin, element);
}
/**
* {@inheritDoc}
*/
@Override
public void disabled() {
try {
super.disabled();
} catch (IllegalStateException e) {
LOG.warn(WARN_MESSAGE);
try {
// using reflection to replicate behavior of
// AbstractModuleDescriptor.disabled()
// as the call to this method was suppressed by
// the exception and there is no way
// to call super.super.disabled()
Field enabledField = AbstractModuleDescriptor.class
.getDeclaredField("enabled");
enabledField.setAccessible(true);
enabledField.set(this, false);
Field moduleClassField = AbstractModuleDescriptor.class
.getDeclaredField("moduleClass");
moduleClassField.setAccessible(true);
moduleClassField.set(this, null);
} catch (Exception e1) {
LOG.error(ERROR_MESSAGE, e1);
}
}
}
}
Die Methode init() wurde überschrieben um die vom Trigger benötigte Scheduler-Bean zu injizieren. Im TriggerModuleDescriptor wird diese Bean durch Autowiring automatisch gesetzt. Da der abgeleitete RobustTriggerModuleDescriptor aber ein anderer Modultyp ist, findet bei dessen Initialisierung kein Autowiring statt.
Um den neuen Modultyp nutzen zu können, muss man ihn zunächst im Plugin-Descriptor (atlassian-plugin.xml) konfigurieren:
<module-type name="Robust Trigger Module Descriptor" key="robust-trigger" class="your.fancy.package.name.RobustTriggerModuleDescriptor"/>
Nun kann man den neuen Modultyp anstelle des Standard-Triggers verwenden:
<robust-trigger key="uniqueKey" name="My Trigger" >
<job key="myJob" />
<schedule cron-expression="0 0 * * * ?" />
</robust-trigger>
Dabei ist zu beachten, dass in Confluence 2.10 die Definition und die Verwendung des neuen Modultyps nicht in dem selbem Plugin erfolgen dürfen. Das geht erst ab Confluence 3.0.
Thorsten Heid von der Firma HLP hat auf dem Confluence Community Day eine Lösung zur Integration zwischen Confluence und SAP Systemen vorgestellt.
Das “SAP Business Package for Confluence Wiki” erlaubt eine Integration des Wikis in das SAP NetWeaver Portal, indem für bestimmte Bereiche Confluence als Service zur Verfügung gestellt wird. Die Lösung berücksichtigt unter anderem SSO zwischen den Anwendungen, das gemeinsame Benutzermamangement, und die Durchsuchbarkeit des Wikis aus dem SAP Portal heraus. Ein vollständiger Überblick über die Funktionalitäten ist in den Vortragsfolien zu finden:
Für mich besonders interessant ist die „tiefe“ Eingliederung des Wikis in die Portalnavigation. Die Links zum Wiki werden kontext- und berechtigungsabhängig angeboten und die Spracheinstellungen des Nutzers im Portal werden für das eingebettete Confluence übernommen. Außerdem werden die in Confluence generierten Links (zum Beispiel in Benachrichtigungen zu einer beobachteten Seite) so angepasst, dass sich das Wiki immer im Look and Feels des Portals und dessen Navigationskontext öffnet. Eine solche Lösung ist sicherlich auch für die Integration von Confluence in andere Drittsysteme spannend.
Für die Einbindung des SAP ERP stellte Herr Heid ein Macro vor, das in Echtzeit Daten aus dem SAP-System in Confluence anzeigt.
Communardo is submitting a Confluence plugin to this years Codegeist competition, powerded by Atlassian.
The Notify&Tag Plugin adds twitter-like functionality to Confluence. It allows to add labels to pages and news by using #hashtags. Additionally users can be notified about interesting content (pages, news and comments) by using the @username notation.
To create a label from a term in the content of a page or news item just precede it with “#” while writing the text. There is no need to provide it again in the according form field: Press “Save” and it will be added to the list of labels.
To notify a user about the page, news or comment you are creating, simply add his username to the text and put “@” in front of it. After saving he will recieve an email containing an excerpt and a link to content. This is a nice feature to attract the attention to relevant information, like the minutes of a missed meeting. You are not requiring to track the activity of the whole site using RSS feeds or the recently-updated macro. Thus, the danger of missing important information is reduced (provided emails are checked on a regular basis
).
The presentation below gives some additional information. For more exhaustive usage dokumentation you can visit the plugin homepage.
Needless to say, we are eager to get your feedback. So feel free to add a comment to this blog or any of the plugin pages and tell us about your opinion or the feature you are missing.
Wie es aussieht ist Atlassian bestrebt die eigenen Produkte stärker mit einander zu verflechten. Neben einer Angleichung der Benutzeroberflächen (z.B. bei JIRA und Confluence) wird auch die Verknüpfung der Inhalte verbessert. So setzt man bei Atlassian auf den OpenSocial Standard, von dem man sich eine bessere Integration der eigenen Produkte untereinander (und mit anderer Software) verspricht. Die Implementation eines OpenSocial-Gadget-Containers in Form eines Dashboards existiert bereits für die aktuelle JIRA 4.0 Beta. Für Confluence ist bereits eine geplant. Um diese Dashboards in Zukunft mit Inhalten aus den anderen Atlassian Anwendungen zu ergänzen, wird es einen neuen Gadget-Plugin-Typ geben.
Aber auch auf technischer Ebene gibt es Bestrebungen die Interoperabilität der einzelnen Produkte zu verbessern. Die Grundlage dafür liefert das neue Plugin-Framework. So stellt es über einen neuen Plugin-Modul-Typ eine einheitliche REST-API bereit, die einzelne Dienste oder Ressourcen der jeweiligen Anwendung über eine URL zugänglich macht.
Außerdem wird es einfacher sein Plugins zu schreiben, die in mehreren Alassian Produkten einsetzbar sind: Mit SAL (Shared Access Layer) gibt es eine Service-Schicht, die eine einheitliche Schnittstelle für einige allgemeine Dienste unabhängig von der jeweiligen Anwendung bereitstellt. Des Weiteren können Plugins so konfiguriert werden, dass bestimmte Module nur für ein bestimmtes Produkt aktiviert sind. Auf diese Weise können Kompatibilitätsprobleme der Modultypen zwischen den einzelnen Anwendungen abgefangen werden.
Vorraussetzung für die meisten der vorgestellten Features, ist die Verwendung von Atlassian Produkten, die das Plugin Framework mit Version 2.2 oder größer verwenden (z.B. Confluence 3.0). Dies ist leider noch nicht bei allen aktuellen Releases der Fall, wie man der Versions-Matrix entnehmen kann.
Die Confluence API bietet über das Bandana-Framework eine einfache Möglichkeit zum Speichern beliebiger Java Objekte an. Im Prinzip muss man zu dem Objekt nur einen einen Kontext (bezieht sich auf einen bestimmten Confluence Space oder ist global) und einen bezüglich dieses Kontextes eindeutigen Key angeben und der BandanaManager kümmert sich um den Rest. Der Vorteil liegt auf der Hand: Man muss sich nicht mit dem Persistenzmodell von Confluence (basiert auf Hibernate) auseinander setzen. Davon rät Atlassian ohnehin ab, Zitat:
Unless you really understand our code, something weird will happen.
Das Speichern von Daten über Bandana geschieht im Wesentlichen in zwei Schritten
Diese Form der Persistenz hat allerdings auch ihre Tücken. Als ich vor Kurzem bei der Entwicklung eines Plugins mal wieder Bandana verwendete bin ich über das folgende Verhalten gestolpert:
Ich hatte zwei Container-Datenstrukturen gespeichert, eine Map und eine List. Nachdem einige kurze Tests positiv verliefen, wendete ich mich einer anderen Komponente des Plugins zu. Um diese zu testen baute ich mein Plugin neu und installierte es über die Weboberfläche (ohne etwas an meiner “Persistenzschicht” zu ändern). Allerdings musste ich nun feststellen, dass eine der Datenstrukturen (die Map) nicht mehr geladen werden konnte. Ein weiterer Test offenbahrte noch wunderlicheres Verhalten: Nachdem ich die Map erneut gespeichert hatte, konnte ich sie wieder auslesen, allerdings nur bis zum nächsten Deploy des Plugins.
Was war passiert? An einem falschen Kontext oder Key konnte es nicht liegen, da diese bereits zuvor das Laden der Map verhindert hätten. Der Verdacht viel schnell auf irgendeinen Cache. Und tatsächlich, nach kurzer Recherche bin ich auf diese Seite bei Atlassian gestoßen. Dort wird beschrieben, dass es eigens für Bandana einen Cache gibt. Meine erste Vermutung war nun, dass die Map aus irgend einem Grund nie den Weg durch den Cache in die Datenbank geschafft hatte, der dann bei der Neuinstallation des Plugins verfiehl. Ein kurzer Blick in die Datenbank und ein wenig Debugging überzeugten mich dann aber vom Gegenteil: Die Daten waren in der Datenbank, schafften es von da aber nicht mehr in den Cache (kurzer Hinweis: die Tabelle für die Bandana-Daten heißt treffender Weise BANDANA). Nach weiterem Debugging stand fest, dass die Daten zwar noch aus der Datenbank geholt wurden, im Anschluss aber die Deserialisierung fehl schlug. Nach einem erneuten Blick in die Datenbank war schnell klar warum. Ich hatte Enums als Schlüssel für die Map verwendet. Diese waren fehlerhaft serialisiert wurden, so dass sie (und mit ihnen die Map) nicht wiederhergestellt werden konnten. Das liegt wohl daran, dass Confluence die XStream Version 1.1.1 verwendet aber Enums erst mit der Version 1.1.2 unterstützt werden. Da die Daten im Cache immer unserialisiert gespeichert werden, fiel dieses Problem erst auf, als der Cache verfallen war. Die Lösung war nun recht einfach: Nach dem Ersetzen der Enums durch Strings verhielt sich mein Plugin wie erwartet.
Enums sind nicht die einzigen Datenstrukturen, mit denen es Probleme gibt. So sollte man es auch vermeiden in einem Plugin definierte Typen mittels Bandana zu speichern, wenn diese keinen Default-Konstruktor haben. Der Grund: Diese Typen würden, wie in diesem Issue beschrieben, beim Deserialisieren mit dem ClassLoader der Web-Applikation gesucht werden. Der kann sie aber nur finden, wenn sie im Classpath der der Web-Applikation liegen, was bei über die Weboberfläche installierten Plugins nicht der Fall ist (dafür hat jedes Plugin seinen eigenen ClassLoader). Also dürften in diesem Fall ebenfalls die oben beschriebenen Probleme auftreten.
Confluence unterstützt standardmäßig keine Authentikation über Single Sign-On Dienste. Im Folgenden soll kurz beschrieben werden wie dies dennoch erreicht werden kann.
Für jeden Seitenaufruf im Confluence-Wiki wird der aufrufende Nutzer benötigt. Um diesen zu ermitteln werden verschiedene Quellen abgefragt (Cookies, Session, …). Erst wenn auf diese Weise kein angemeldeter Nutzer festgestellt werden kann, wird die Login-Seite angezeigt. Der vollständige Ablauf der Authentikation kann im Confluence Development Hub nachgelesen werden. Diese Ermittlung des Nutzers erfolgt durch einen sog. Authentikator. Der muss nun so angepasst werden, dass er mit dem zu unterstützenden Single Sign-On Dienst zusammenarbeitet (also den über diesen Dienst angemeldeten Nutzer ermittelt). Dazu schreibt man am besten einen eigenen Authentikator der den Confluence Standardauthentikator beerbt und passt die entscheidenden Methoden an.
Die hier vorgestellte Lösung basiert darauf, dass der Name des bereits am Single Sign-On Dienst authentifizierten Nutzers dem Servlet Container von Confluence bereitgestellt wird. Damit kann er über das Request-Objekt ermittelt werden (HttpServletRequest.getRemoteUser()). Dies ist für jedes Single Sign-On System auf andere Weise einzurichten und verursacht auch den meisten Aufwand. Um beispielsweise eine Anbindung an einen Kerberos-Dienst zu realisieren ist folgendes Vorgehen denkbar:
Um nun die Authentikation gegen den weitergereichten Nutzernamen durchzuführen ist nur die Methode getUser() des Standardauthentikators (ConfluenceAuthenticator) zu überschreiben:
package my.auth.package;
public class MySSOAuthenticator extends ConfluenceAuthenticator{
public Principal getUser(HttpServletRequest request, HttpServletResponse response) {
Principal principal = null;
// check if the user is already logged in (cookies & co.)
principal = super.getUser(request, response);
if (principal == null) {
String remoteUserName = request.getRemoteUser();
if (remoteUserName != null && remoteUserName.trim() != "") {
// try to get the user's account using his name
principal = this.getUser(remoteUserName);
if (principal != null) {
// success, some additional code here (add user to session, ...)
} else {
// some error handling as user can't be acquired from request
}
}
}
return principal;
}
}
Nun muss nur noch der Standardauthentikator durch den neuen Authentikator ersetzt werden. Dies ist zum Glück mit wenig Aufwand machbar:
...
<authenticator class="my.auth.package.MySSOAuthenticator"/>
...
Nach einem Neustart von Confluence sollte nun der neue Authentikator verwendet und somit Single Sign-On unterstützt werden.
Releaseparty at Atlassian? Confluence 3.2 BETA and 3.1.2 with soms bugfixes were released yesterday. [...]
Tino Schmidt's Vortrag zu Enterprise Mashups auf der webciety, 4.3 Remix the Web http://bit.ly/d26rtA [...]
neuer Blogpost: February Cumulative Update (2010) http://bit.ly/cwxZGE [...]
Webinar am 16.03.: „Communote Enterprise Microblogging - Funktionen und Einsatzbereiche im Unternehmen“ http://bit.ly/96eexF [...]