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.
Ein Teil unserer Kernkompetenzen im Umgang mit Sharepoint, ist die Migration von Altsystemen zu Sharepoint. Ein aktuelles Projekt stellte uns vor die Herausforderung, Inhalte, Stylevorgaben und Meta-Daten mittels XML Import aus einem bestehenden Content Management System 1:1 nach SharePoint zu übernehmen. Besonders problematisch war dabei der Fakt, dass sich innerhalb der Daten Parameter für die Businesslogik versteckten. Diese Logik musste in SharePoint zum Teil nachgebaut und beim Import berücksichtigt werden.
Eine Teilaufgabe des Imports bestand in der Übernahme der eigentlichen Inhalte der Seiten. Diese lagen als barrierefreies HTML vor und mussten deshalb original so in die SharePoint Seite importiert werden.
Meine Aufgabe bestand nun darin, den originalen HTML Inhalt der zu migrierenden Seite in eine Sharepoint Seite zu importieren. Möglichst sollten die Optik (Styles, Bilder, etc.) sowie der Inhalt (inkl. Links, Tabellen etc.) unangetastet bleiben.
Sharepoint bietet dem geneigten Entwickler mit seiner API reichlich Werkzeug um diese Aufgabe zu bewältigen. So kann eine Seite ohne Probleme mit folgenden Programmcode angelegt werden:
PublishingWeb currPublishingWeb = PublishingWeb.GetPublishingWeb(webContext);
PublishingPageCollection pages = currPublishingWeb.GetPublishingPages();
PublishingPage currentPage = pages.Add(pageFileName, layout);
Auf diese Weise ist eine Publishing Page schnell erstellt. Soll diese jetzt auch noch mit Inhalt versehen werden, wird das SPField, das für den Inhalt einer PublishingPage zuständig ist, benötigt.
Das SPField für Inhalte ist das PublishingPageContent Field, das über die FieldId Klasse verwendet werden kann.
SPListItem newFileItem = newFile.Item;
newFileItem[FieldId.PublishingPageContent] = htmlContent;
newFileItem.Update();
Bis hier wurde alles ordnungsgemäß von Sharepoint ausgeführt. Die Seite wurde angelegt und der Inhalt wurde auch gesetzt. Beim näheren Betrachten der neu Erstellten Seite wurde ich jedoch misstrauisch:
Durch dieses Verhalten wurden die importierten Seiten “wertlos” für mich. Alle darauf folgenden Versuche den Inhalt in die Seite zu schreiben schlug fehl. Versucht habe ich folgende Wege:
Da ich das Problem gerne ohne “Dirty Hacks” lösen wollte, entschied ich mich eine Supportanfrage bei Microsoft zu stellen. Dazu sei erwähnt, dass die Anfragen über den Microsoft Support sehr schnell und kompetent beantwortet und abgewickelt werden. Ich sollte meine Antwort von Microsoft bekommen; Leider hieß diese “By design” oder kurz auf deutsch: abgewiesen.
Die Begründung: Mirosoft Sharepoint nutzt einen sogenannten XSS (Cross side scripting) Protection Mechanismus. Dieser sollte verhindern das schädlicher Inhalte (Code) in Sharepoint eingepflegt werden kann.
Davon betroffen ist allerdings auch das HTML Texteingabe Control. Beispielsweise würde dieser Programm – Code wie folgt abgeändert:
original: Das ist ein gutes <SCRIPT>void:alert("hello world")</SCRIPT> Script
verfälscht: Das ist ein gutes Script
Auch “<” oder “>” Zeichen würden wie folgt abgeändert: > – <. Ich empfinde dieses Verhalten als vollkommen richtig und auch nachvollziehbar, wenn es dabei bleiben würde. Es stellte sich herraus dass dieser XSS Filter auch für die Kürzung meiner HTML Inhalte zuständig war. Microsoft selbst kann sich nicht erklären warum dieser Filter solch drastische Änderungen am HTML Quellcode vornimmt. Man riet mir von Seiten Microsoft ernsthaft entweder ein 3rd Party AddOn als Ersatz für den RichTextEditor einzusetzen oder aber die Daten nicht innerhalb Sharepoints zu halten, sondern extern. Keiner der beiden angebotetenen Lösungen erschien mir für unser Projekt auch nur ansatzweise logisch beziehungsweise umsetzbar.
Um die Inhalte dennoch wie gefordert 1:1 importieren zu können, musste nun ein Workaround gefunden werden. Dazu bot sich eine Codierung der beim Import beanstandeten HTML-Tags an. Nach der erfolgreichen Integration der Methoden in unseren Import können nun alle Inhalte wie gefordert übernommen werden.
Beim programmatischen Erstellen von benutzerdefinierten Spalten (SPField) in Sharepoint sind einige Dinge zu beachten und erwarten auch beachtet zu werden. Hält man sich jedoch an die Spielregeln (API Dokumentation) und erzielt trotzdem nicht das gewünschte Ergebniss, ist wahrscheinlich wieder ein Sharepoint Bug gefunden worden. Dies musste ich heute wieder mal schmerzhaft feststellen.
Sharepoint verwendet zwei Namen um Felder zu kennzeichnen. Den internalName sowie den displayName (bzw. Title). Der internalName wird von Sharepoint selbst sowie von dem Sharepoint Objekt Modell verwendet um Spalten(SPField) und Objekte wieder zu erkennen und anzusprechen. Der displayName und die Title – Eigenschaft werden verwendet um die Darstellung der Spalten zu steueren. So können eindeutige Namen Verwendung finden um Objekte eindeutig anzusprechen. Gleichzeitig kann der Anzeigename variabel gestaltet werden.
So hatte ich versucht eine Sharepoint Spalte über die API zu erstellen und anschließend zur SPFieldCollection einer Liste hinzuzufügen.
Dies funktionierte mit den folgenden Codeschnipseln auch sehr gut.
SPField currField = colFields.CreateNewField(SPFieldTypeString, displayName);
currField.staticName = internalName;
colFields.AddFieldAsXml(currField.SchemaXml, true, SPAddFieldOptions.AddToAllContentTypes);
Anschließend beim Beschreiben der Spalte gingen jedoch die Probleme los.
SPField newField = newItem.Fields.GetFieldByInternalName(internalFieldName);
Laut API Dokumentation und MSDN welche besagt, dass der staticName dazu dient, um den internalName zu setzen, wäre der obige Programm – Code richtig. (“Gets or sets the internal name of the field.”)
Das Ausführen der Methode verursachte eine ArgumentException mit der Meldung: Value does not fall within the expected range.
Darfaufhin begann ich ein wenig weiter zu forschen (Danke an Sharepoint Manager) und fand herraus, dass die AddFieldAsXml Methode in Verbindung mit einem SPField welches mit CreateNewField erzeugt wurde einen Bug von Sharepoint hervorruft. AddFieldAsXml setzt den displayName wie angegeben, aber leider auch den internalName mit dem Wert des display Namens. Das hat zur Folge, dass die Spalten nicht mehr über den interalName ansprechbar sind.
Wer jetzt also vor genau diesem Problem stehen sollte, hat zwei schnelle Möglichkeiten.
Workaround:
Die Lösung des Problems ist regelrecht simpel. Es wird der gleiche obige Code verwendet um das SPField zu erstellen, jedoch als displayName der internalName gesetzt. Dadurch legt Sharepoint die Spalte mit dem richtigen internalName an. Sobald die Spalte angelegt wurde, muss nur noch der displayName geändert werden.
string internalName = "internerName";
string displayName = "displayName";
SPField currField = colFields.CreateNewField(SPFieldTypeString, internalName);
colFields.AddFieldAsXml(currField.SchemaXml, true, SPAddFieldOptions.AddToAllContentTypes);
SPField rewriteField = colFields[internalName];
rewriteField.Title = displayName;
rewriteField.Update();
Wird die GetField- oder GetFieldByInternalName - Methode jetzt mit dem internalName aufgerufen liefert sie die gewünschte Spalte.
Heute habe ich versucht, über ein Nutzerinterface (GUI) eine Site Collection (Websitesammlung) anzulegen, welche über eine eigene Url erreichbar ist (Host Named Site Collection). Wie erwartet ging dies nicht ohne jegliche Komplikationen über die Bühne.
Um eine Host Named Site Collection anzulegen, stehen dem Entwickler zwei Varianten zur Verfügung. Die erste und von mir bevorzugte ist die programmatische:
Mit der folgenden Methode (z.B. in einer Konsolenanwendung) kann eine Host Named Site Collection angelegt werden:
void createHostNamedSC(string siteUrl,string siteName,string siteDescription)
{
SPWebApplication webApp = SPWebApplication.Lookup(new Uri(SPContext.Current.Web.Url));
SPSiteCollection sites = webApp.Sites;
SPSite Site = null;
Site = sites.Add(siteUrl, siteName, siteDescription, 1033, “STS#0″, “Domain\\Administrator”, “Owner_Display_Name”, “Owner_Email”, “Domain\\Administrator”, “Secondary_Owner_Display_Name”, “Secondary_Owner_Email”, true);
}
Die zweite Variante wird über stsadm realisiert:
Dazu wird in der Console folgender Aufruf gestartet:
stsadm.exe -o createsite
-url http://hnsc.webapplication.com
-ownerlogin Domain\Administrator
-owneremail Administrator@webapplication.com
-hhurl http://www.webapplication.com
Die Webapplication stellt dabei der Eintrag hinter -hhurl (http://www.webapplication.com) dar.
Unter -url wird die gewünschte URL der neuen Site Collection angegeben.
Hat man sich für eine der beiden Methoden entschieden und diese ausgeführt, müssen noch die Hostheader für die Webapplikation angepasst werden. Das Hinzufügen der Hostheader kann einerseits über die Sharepoint Zentral-Administration erfolgen oder aber über den Internet Information Services (IIS) Manager.
Ich habe mich dabei für die Einstellung innerhalb des IIS entschieden.
Dazu öffnet man den IIS Manager und öffnet über das Kontext-Menü die Einstellungen (Properties) der Webapplikation.


Nach diesen Schritten sollte die eben erstelle Site Collection unter ihrer Domain aufrufbar sein.
Nachdem ich diese Schritte alle erfolgreich ausgeführt hatte, versuchte ich mich nun an meiner neuen Host Named Site Collection anzumelden. Obwohl ich augenscheinlich alle Credentials ordnungsgemäß eingegeben hatte, bekam ich folgende Seite zu sehen:
HTTP 401.1 – Nicht autorisiert: Fehler bei der Anmeldung

Nachdem ich schon fast anfangen wollte, mir die Haare auszureißen, fand ich eine Bugbeschreibung von Microsoft unter
Fehler 401.1 beim Aufrufen einer Website
Update:
Spencer Harbar weißt auf seinem Blog, in einen interessanten Artikel, darauf hin, dass dieser Bugfix nicht auf Produktiv – Maschinen eingesetzt werden sollte. Für den externen Zugriff auf die Site Collection tritt dieses Problem in der Regel nicht auf. Deshalb richtet sich dieser Fix nur an Entwickler welche auf der selben Maschine mit mehr als einer Host Named Site Collection arbeiten.
Laut Microsoft tritt dieser Fehler nur auf, wenn die Website integrierte Authentifizierung verwendet und ihr Name der lokalen Loopbackadresse zugeordnet ist. Wenn dann noch Windows Server 2003 mit SP1 installiert ist, schlägt der Fehlerteufel zu. Demnach schlägt die Authentifizierung fehl, wenn der Domänenname oder der benutzerdefinierte Hostheader nicht mit dem lokalen Computernamen übereinstimmen.
Um den Bug zu beheben, müssen folgende Schritte ausgeführt werden.
Nach dem Neustart war ich in der Lage, die Site Collection unter ihrer Domain aufzurufen.
Möchte man den ModalPopupExtender aus dem ASP.NET AJAX Control Toolkit verwenden, dann geht das nur mit dem DOCTYPE “XHTML 1.0 Transitional” so richtig reibungslos. Beim CollapsiblePanel gibt es dazu einen Hinweis in der Dokumentation, beim ModalPopup fehlt dieser Hinweis. Das Problem besteht aber auch dort.
Symptome: Das Popup wird nicht korrekt positioniert, zentriert sich nicht und viel zu lange Scrollbalken entstehen.
Gerade wenn man WebParts für SharePoint erstellt, hat man aber nicht immer Einfluss auf den DOCTYPE. Oder wenn man schon Einfluss auf den DOCTYPE hat, kann man ihn nicht immer einfach so ändern, weil dann evtl. andere Komponenten nicht mehr richtig funktionieren oder nicht mehr richtig dargestellt werden.
Der beschriebene Workaround aus der Dokumentation des AJAX Control Toolkit fällt damit als Lösung für uns aus.
Ein anderer Lösungsweg ist, ein Custom Build des ASP.NET AJAX Control Toolkit zu erstellen und darin das “fehlerhafte” Stück JavaScript zu korrigieren.
Dazu besorgt man sich den Quellcode via Codeplex und nimmt die folgenden zwei Modifkationen vor:
switch (Sys.Browser.agent) {
case Sys.Browser.InternetExplorer:
if (document.documentElement && document.documentElement.clientWidth)
clientWidth = document.documentElement.clientWidth;
else if (document.body)
clientWidth = document.body.clientWidth;
//clientWidth = document.documentElement.clientWidth;
if (document.documentElement && document.documentElement.clientHeight)
clientHeight = document.documentElement.clientHeight;
else if (document.body)
clientHeight = document.body.clientHeight;
//clientHeight = document.documentElement.clientHeight;
break;
case Sys.Browser.Safari:
clientWidth = window.innerWidth;
clientHeight = window.innerHeight;
break;
case Sys.Browser.Opera:
clientWidth = Math.min(window.innerWidth, document.body.clientWidth);
clientHeight = Math.min(window.innerHeight, document.body.clientHeight);
break;
default: // Sys.Browser.Firefox, etc.
clientWidth = Math.min(window.innerWidth, document.documentElement.clientWidth);
clientHeight = Math.min(window.innerHeight, document.documentElement.clientHeight);
break;
}
Diese Modifikation bringt dem Toolkit bei, Höhe und Breite ein bisschen besser auszulesen.
Achtung:
Hat man bereits an anderen Stellen im eigenen Code Anpassungen aufgrund dieses Problems vorgenommen, indem man z.B. via CSS oder JavaScript nachträglich die Position eines AutoComplete Feldes korrigiert, so ist dies jetzt möglicherweise nicht mehr notwendig und/oder funktioniert nicht mehr richtig. Hier also nochmal kontrollieren!
this._backgroundElement.style.position = 'fixed';
ersetzen zu
this._backgroundElement.style.position = 'absolute';//'fixed';
Ein paar Zeilen weiter unten:
his._foregroundElement.style.position = 'fixed';
ersetzen zu
this._foregroundElement.style.position = 'absolute';//'fixed';
Diese Modifikation beeinflusst andere Controls nicht, und sorgt nur für die ordentliche Positionierung des Popups.
Außerdem zu beachten:
Wenn man das Control Toolkit mit dem mitgelieferten Schlüssel kompiliert, so wir der selbe PublicKeyToken wie beim Original erzeugt. Daraus entstünde dann möglicherweise folgendes Problem:
Verwenden andere Komponenten auf dem Server auch das AJAX Control Toolkit, so bekommen sie die oben vorgenommenen Änderungen durchgereicht, sofern sie nicht explizit gegen eine bestimmte Version kompiliert wurden.
Man sollte also lieber einen neuen PublicKeyToken erzeugen, um möglichen Kompatibilitätsproblemen im Voraus zu entgehen.
Quelle:
Ramesh Bhaskar – Fixing modalpopupextender position problems
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 [...]