Montag, 13. Mai 2013

ObjectStudio 8 New UI: DateTimePicker (Part 1)

The DateTimePicker (Class UIDateTimePicker) is the first control I am going to have a more detailled look at.

Open a demo like this:

TestUI new testOpenUIDateTimePicker

Do not forget to call UIView initialize before.


We now add the functionality to set a Date before opening the control and to enable the DTS_SHOWNONE style.

Adding a value: method

I do not know how exactly the new UI API to set values will look like, so I just implement a value: method to try it out.

value: aTimestamp
    self dateOrTime: aTimestamp.
    self setSystemTime: aTimestamp.

setSystemTime: aTimestamp
    self privateSetSystemTime: aTimestamp.

 privateSetSystemTime: aTimestamp
    | st res |

    st := self lib SYSTEMTIME gcCalloc.
    aTimestamp isNil ifFalse: [
        st 
            memberAt: #wYear put: aTimestamp date year;
            memberAt: #wMonth put: aTimestamp date months;
            memberAt: #wDay put: aTimestamp date dayOfMonth;
            memberAt: #wHour put: aTimestamp time hours;
            memberAt: #wMinute put: aTimestamp time minutes;
            memberAt: #wSecond put: aTimestamp time seconds;
            memberAt: #wMilliseconds put: aTimestamp time milliseconds.
        ].

    res := self lib
                SendMessage: self windowHandle
                msg: DTM_SETSYSTEMTIME
                wParam: (aTimestamp isNil ifTrue: [GDT_NONE] ifFalse: [GDT_VALID])
                lParam: (st ifNotNil: [st referentAddress]).
    ^res

create
    super create.
    dateOrTime ifNotNil: [
        self setSystemTime: dateOrTime.
    ].

Adding the DTS_SHOWNONE style

To add the DTS_SHOWNONE style we need to add it to the styles before creating the control. Setting it afterwards does not seem to work. In the example code we will close and create the control when the setShowNoneTo: method is called and the control is already open. Better would be to only set this style before creating it, maybe the Designer for the new UI will include some options to add styles.

Closing and (re-)creating the same control does not work in the current preview because the hwnd instance variable is not set to nil after destroying the view (I am not shure if this is by intent or not). So I added the following overwrite to UIView:

UIView>>close
    self privateClose.
    self privateDestroy.
    self hwnd: nil.

In the Date and Time Picker control you need to check some results for GDT_NONE (Checkbox is unset) and GDT_VALID (Checkbox is set and the Date Time is valid). Here is the complete code for UIDateTimePicker.

setShowNoneTo: aBoolean
    aBoolean ifTrue: [
        "SetStyle does not change the style attribute"
        self setStyle: DTS_SHOWNONE.
        self style: (self style bitOr: DTS_SHOWNONE).
    ] ifFalse: [
        self removeStyle: DTS_SHOWNONE.
        self style: (self style bitAnd: DTS_SHOWNONE bitInvert).
    ].

    "Seems we need to recreate to apply this style"
    self isOpen ifTrue: [
        self close; create.
    ].

getShowNone
    self isOpen ifFalse: [^false].
    ^(self privateGetStyle bitAnd: DTS_SHOWNONE) > 0

privateGetSystemTime
    | st res |
    st := self lib SYSTEMTIME gcCalloc.
    res := self lib
                SendMessage: self windowHandle
                msg: DTM_GETSYSTEMTIME
                wParam: 0
                lParam: st referentAddress.
    ^res == GDT_VALID
        ifTrue: [self lib timestampFromSysTime: st contents]
        ifFalse: [nil]

privateDateTimeChange: aNotifyEvent
    | struct oldTs |
    struct := self lib LPNMDATETIMECHANGE cast: aNotifyEvent lParam.

    (struct memberAt: #dwFlags) == GDT_NONE
        ifTrue: 
            [oldTs := self dateOrTime.
            self dateOrTime: nil.
            oldTs ifNotNil: [self announce: (UIChangeAnnouncement before: oldTs after: self dateOrTime)]].

    (struct memberAt: #dwFlags) == GDT_VALID
        ifTrue: 
            [oldTs := self dateOrTime.
            self dateOrTime: (self lib timestampFromSysTime: (struct memberAt: #st)).
            self dateOrTime = oldTs
                ifFalse: [self announce: (UIChangeAnnouncement before: oldTs after: self dateOrTime)]].

Test it

Write a Test class method in TestUI like:

testOpenUIDateTimePickerEx
    | view value |

    value := (Timestamp fromDate: {2000 1 1} asDate andTime: (Time fromSeconds: 0)).
    
    view := self new testOpenUIDateTimePicker.
    view ctrl1 value: value. 

    view ctrl1 setShowNoneTo: true.
    self new ctrl1 getSystemTime out.
    
    view ctrl1 value: nil.
    self new ctrl1 getSystemTime out.

    view ctrl1 setShowNoneTo: false.
    self new ctrl1 getSystemTime out.

    self new testCloseUIDateTimePicker


Or try in using the workspace.


Next I will show how to change the display style (short, long).

Download code from here.

ObjectStudio 8 New UI

The newest preview version of ObjectStudio 8.5 does include a preview package for the new UI framework. You must load the package using the parcel manager (Directories => parcels/preview)

The new UI is based on the Win32 APIs, MFC seems not to be used any more.

The base class is ObjectStudio.UIView. There is a class TestUI which one can use as a demo controller. The API calls are implemented in ObjectStudio.UIView.UILib.

To display a new empty window without any icons:

UIView initialize.
UIView new create.


 

I had to change the create and privateCreate methods in UIView to get this working.

create
    self isOpen ifTrue: [^self].
    UIView critical: 
            [self class createWindow: self.
            "hhk := self hookWindowCreate." 
            [self privateCreate] ensure: 
                    [self class createWindow: nil.
                    "self unhookWindowCreate: hhk"]]

privateCreate

    | aHwnd |
    aHwnd := self lib
                CreateWindowEx: self wndExStyle
                class: (self class wndClassName isString
                        ifTrue: [self class wndClassName gcCopyToHeapUnicode]
                        ifFalse: [self class wndClassName])
                window: self windowName gcCopyToHeapUnicode
                style: self wndStyle 
                x: self position x
                y: self position y
                width: self extent x
                height: self extent y
                parent: (self parent isNil ifTrue: [0] ifFalse: [self parent windowHandle])
                menu: 0
                hInst: 0
                lParam: 0.

    self windowHandle: aHwnd datum .
    self registerWindow: aHwnd datum.

    aHwnd datum = self windowHandle
        ifFalse: 
            [(UICreateWindowExError new)
                originalHwnd: self windowHandle;
                newHwnd: aHwnd datum;
                raiseSignal]


If you want a window with the standard windows icons:

(UIView new)
    style: UIView defaultStyle;
    create;
    setDefaultEvents.


In the TestUI class they do it a bit different (also this code does not display the left icon):

TestUI new testOpenWindow


Download code from here.
See also this post from Cincom.

Xpert.Ivy unter Debian mit Postgresql installieren

Eine kurze Anleitung wie man Xpert.Ivy 4.3.9 unter Debian Linux 6.0 "Squeeze" mit einer Postgresql Datenbank installieren kann.

Das folgende wurde auf einem gehosteten VServer von nine.ch durchgeführt.

Als erstes muss Postgresql installiert werden mit:

# apt-get install postgresql postgresql-client

Um später die Xpert.Ivy Systemdatenbank erstellen zu können, müssen die Init-DBs von Postgres mit UTF-8 Encoding erstellt werden. Da dies bei mir nach der Installation der Packages nicht so war, musste ich das von Hand durchführen.

Zuerst prüfen wir die vorhandenen (deutschen) Locales:
# locale -a | grep de_ 

Anschliessend löschen wir die bei der Installation erstellte Datenbank und erstellen sei neu:

# su postgres
# pg_dropcluster --stop 8.4 main
# pg_createcluster --locale de_CH.UTF-8 --start 8.4 main

Nun muss noch die "plgsql" Sprache installiert werden:

# su postgres
# createlang plpgsql template1


Und zum Schluss muss noch ein Benutzer für Xpert.Ivy erstellt werden:

# su postgres
# createuser ivy
# psql alter user ivy password 'x'


Admin Remote Zugriff
- pgAdmin downloaden
- apt-get install postgresql-contrib
- psql postgres -f adminpack.sql
- postregsql.conf => listen *
- pg_ => host hinzuguegen

 Xpert.Ivy konfigurieren
- wget http://...zip - unzip installieren ?
- Lizenzdatei kopieren
- ./XpertIvyServerConfig.sh -headless

 Links
http://wiki.debian.org/PostgreSql
 http://supriyadisw.net/installing-postgresql-on-debian.html

Montag, 7. Januar 2013

Fälle für Benutzer selektieren

Ich habe letzte Woche einen Prozess so umgestellt, dass er als reiner Hintergrund-Prozess als Benutzer SYSTEM startet. Ein Problem das sich dadurch ergab war, dass die Prozesse in unserem angepassten WorkflowUI nicht mehr in der Fall-Übersicht des Benutzer als "Meine"-Prozesse erschien.

Die eigentliche Selektion findet in der Methode findCases in der Klasse ch.ivyteam.ivy.workflow.ui.utils.WorkflowUIAccessPermissionHandler statt. Dieser Methode kann ein Integer "caseDisplayMode" mitgegeben werden, welcher den Benutzer-Filter steuert:
  • 0 stands for your cases
  • 1 stands for team cases
  • 2 stands for all application cases (ivy.wf)
  • 3 any query cases: it means find all cases that fit to the received criteria (property filter)

Um "Meine"-Prozesse anzuzeigen, wird der Wert 0 übergeben. Die Methode findCases verwendet dann die Ivy-Session Methode findInvolvedCases. Als Involviert gilt ein User wenn:
  • he is the creator of the case
  • he has worked or is working on a task of the case.
  • he can work on a task of the case. Either because he is the activator of the task or the activator of the task is a role he owns.
Wird nun ein Prozess komplett im Hintergrund gestartet, z.Bsp. über ein Event Start, und läuft anschliessend weiter im Hintergrund (als Benutzer SYSTEM), kann er daher nicht direkt einem Benutzer zugeordnet werden.

Zum Ändern des Erstellers eines Cases habe ich keine API Methode gefunden. Daher habe ich momentan folgendes implementiert:
  • Beim Starten des Prozesses über ein File basierter Event Start wird der "Besitzer" des Prozesses aus der Datei gelesen (wurde in diesem Fall bereits mitgeliefert) und in den Case Daten (hier als businessCreatorUser) gespeichert
  • Bei der Anzeige habe ich eine zusätzliche Option "Besitzer" implementiert, welche als caseDisplayMode 3 angibt und einen entsprechende Filter übergibt:
IPropertyFilter filter = in.propertyFilter;
if(in.caseDisplayMode == 3) {
    filter =  ivy.wf.createCasePropertyFilter(
       ch.ivyteam.ivy.workflow.CaseProperty.BUSINESS_CREATOR_USER,
     ch.ivyteam.logicalexpression.RelationalOperator.EQUAL,
     ivy.session.getSessionUserName());
}

Dienstag, 18. Dezember 2012

Xpert.Ivy Query Interface für Tasks und Cases

Bin gerade auf das "fluent" Query-Interface gestossen um Tasks und Cases zu suchen.

Hier ein Beispiel um alle Tasks in einem bestimmten Status zu finden:

import ch.ivyteam.ivy.workflow.TaskState;
import ch.ivyteam.ivy.workflow.ITask;
import ch.ivyteam.ivy.workflow.query.TaskQuery;

TaskQuery query = ivy.wf.getTaskQueryExecutor().createTaskQuery()

   .where().state().isEqual(TaskState.FAILED)

   .orderBy().name();

List<ITask> results = ivy.wf.getTaskQueryExecutor().getResults(query);
for(ITask task : results) {
    // Do something
}


Zum Vergleich das "findTasks"-API:

import ch.ivyteam.ivy.workflow.PropertyOrder;
import ch.ivyteam.ivy.workflow.IPropertyFilter;
import ch.ivyteam.ivy.workflow.TaskState;
import ch.ivyteam.ivy.workflow.ITask;
import ch.ivyteam.ivy.persistence.IQueryResult;

IPropertyFilter filter = ivy.wf.createTaskPropertyFilter(
    ch.ivyteam.ivy.workflow.TaskProperty.STATE,
    ch.ivyteam.logicalexpression.RelationalOperator.EQUAL,
    TaskState.FAILED);
List<PropertyOrder> order;
order.add(new PropertyOrder(ch.ivyteam.ivy.workflow.TaskProperty.NAME));

IQueryResult result = ivy.wf.findTasks(filter,order,0,-1,true);

for(ITask task : result.getResultList()) {
    // Do something
}


https://sourceforge.net/p/xivy/wiki/IvyCookbook/
http://www.ivyteam.ch/downloads/XIVY/4.3.0/doc/PublicAPI/ch/ivyteam/ivy/persistence/query/package-summary.html


Freitag, 14. Dezember 2012

Smalltalk Cheat Sheet

Cincom hat eine kurze Übersicht zu Smalltalk veröffentlicht:

"Cincom Smalltalk – The Language on Two Pages"


SQLServer log keeps growing

Recently our database log was growing bigger and bigger on our main development database. The DBA just blamed the developers because he meant they were not closing transactions correctly.

But DBCC OPENTRAN did not display any open transactions, but instead reported that SQLServer tried to replicate transactions, also we did not have any replication configured for this database.
DBCC OPENTRAN
Transaktionsinformationen für die ...-Datenbank.

Replizierte Transaktionsinformationen:
Älteste verteilte LSN : (0:0:0)
Älteste nicht verteilte LSN : (214984:95479:1)
Using fn_dblog I found the statement which belonged to the mentioned LSN (214984:95479:1 is in HEX 347C8:174F7:1)
select top 100 [Current LSN], [Begin Time], [Transaction Name], [AllocUnitName] from ::fn_dblog( null, null)

000347c8:000174f7:0001 2012/11/25 14:31:18:997 CheckDb NULL
000347c8:000174f7:0002 NULL NULL Unknown Alloc Unit
000347c8:000174f7:0003 NULL NULL ...
I found some information in the SQLServer Logs at this time that showed that CHECKDB did have some problems at this time, maybe it was even crashing, probably leaving the database in a problematic state.

Unfortunately I do not know how our DBA solved the problem, but at least he stopped blaming the developers ;-)

You will find some informations on the web about similar problems, search Google for sp_removedbreplication or sp_repldone.

This query may also help to find out why log space is not reused:
select name, log_reuse_wait, log_reuse_wait_desc from sys.databases
Also check out these links:

Mittwoch, 28. November 2012

System Integration mit Xpert.Ivy: Intermediate Events über Dateien

Für die asynchrone Kommunikation zwischen Xpert.Ivy und externen Systemen innerhalb eines Prozesses stellt Xpert.Ivy Intermediate Events zur Verfügung.

Ein mögliches Szenario könnte wie im folgenden Bild sein. Eine Geschäftsapplikation sendet eine Meldung an Xpert.Ivy um einen Prozess zu starten. Im Laufe des Prozesses sendet Xpert.Ivy an ein Druckoutput System eine Anfrage zum Aufbereiten und Versenden eines Formulares. Anschliessend wartet der Prozess auf eine Meldung vom DMS System, dass das Formular zurückgesendet und archiviert wurde.




Und so könnte das in Xpert.Ivy aussehen:





Wie  ein Prozess Start über eine Datei funktionieren kann, habe ich in diesem Post beschrieben:
System Integration mit Xpert.Ivy: Event Start über Dateien

Intermediate Events

Jedem Intermediate Event muss in Xpert.Ivy eine eindeutige ID, genannt "Event ID", zugewiesen werden. Dies kann die Ivy Case ID sein, oder eine andere eindeutige ID.Wenn man die Case ID nimmt, muss man während dem Entwickeln mit dem Designer etwas aufpassen, da nach jedem Start der Engine wieder mit der Case ID "1" begonnen wird.

Im oben beschriebenen Szenario würde beim Print-Request bereits die gleiche Event-ID an das Druck-System übergeben. Dieses könnte die Event-ID als Strichcode auf ein Formular drucken, welcher dann wenn das Formular zurückkommt, vom Scanner bzw. Dokument Management System gelesen und wieder in die Event-ID umgewandelt wird. Diese wird dann vom DMS wieder an Xpert.Ivy zurückgegeben, wo so die Event-ID wieder einem bestimmten Prozess zugeordnet werden kann.

Hier das etwas überarbeitet Szenario.


Xpert.Ivy liefert die Klasse FileIntermediateEventBean aus, welche wir uns im folgenden etwas genauer ansehen.

FileIntermediateEventBean

Die Klasse FileIntermediateEventBean nimmt eine Datei im folgenden Format entgegegen:

Attribute1=Wert1,Attribute2=Wert2,...

Die Werte können über die Ivy Variable "result" im Output-Tab des Intermediate Events abgefragt werden.

Beispiel:


Mittwoch, 14. November 2012

Smalltalk versus Java

Als Entwickler im Umfeld von Geschäftsapplikationen mit Smalltalk und Java Erfahrung möchte ich hier einen kleinen Vergleich zwischen den beiden Sprachen bzw. Plattformen machen.

Die Geschichte von Smalltalk beginnt irgendwann in den 70er Jahren, wobei es erst in den 80er und 90er Jahren vermehrt Verbreitung fand, um dann mit dem Aufstieg von Java wieder Marktanteile zu verlieren. Heute sind Smalltalk-Projekte (zumindest in der Schweiz) ziemlich selten geworden.

Hier noch ein Zitat zu den Ursprüngen von Java:
Java. The elegant simplicity of C++. The blazing speed of Smalltalk.
http://c2.com/cgi/wiki?SmalltalkMinusMinus
Zum beginnen einige Gemeinsamkeiten von Smalltalk und Java, quer über Sprache und Plattform:
  • Beide sind Objekt-Orientiert
  • Beide haben eine grosse Klassenbibliothek z.Bsp. für Collections
  • Beide laufen in einer Virtual Machine und verwenden ein Garbage Collector
  • Viele Smalltalk VM's laufen wie die JVM auf mehreren Betriebssystemen
Und hier einige Unterschiede:
  • Smalltalk verwendet keine statische Typisierung
  • Smalltalk hat nur sechs Key-Wörter, alles andere wird über Klassen, Objekte, Methoden und Blöcke (entspricht in etwa den Closures in Java 8) abgebildet
  • Smalltalk verwendet sogenannte Images anstatt Dateien
  • In Smalltalk bilden Sprache, Runtime und IDE eine stärkere Einheit. Dafür gibt es nicht wirklich einen Hersteller übergreifenden Standard, ein Wechsel der IDE ist praktisch nicht möglich
In der täglichen Arbeit wirken sich diese Unterschiede mal weniger, mal mehr aus.

Statische versus Dynamische Typisierung

Als Nachteil von dynamischer Typisierung wird häufig angeführt, dass dadurch zur Laufzeit ein falsches Objekt verwendet werden könnte. In der praktischen Arbeit kommen solche Fehler aber extrem selten vor, wenn überhaupt. Viel häufiger tretten aber auch in Smalltalk folgende Probleme auf: Null Pointer Exceptions.

Umbenennen von Klassen oder Methoden geht in Java etwas einfacher (vorausgesetzt die ganzen Verweise liegen im Projekt oder Workspace vor), weil beim Compilieren Fehler auftreten. Dafür spart man in Smalltalk einiges an Zeit weil es keine merkbaren Compilierzeiten gibt.

Sprache und Klassenbibliothek

Was einem als Smalltalk Entwickler beim Einstieg in Java vermutlich als erstes auffällt, ist der grosse Aufwand den es zu Beginn braucht um nur einmal sämtliche Schlüsselwörter und Konstrukte der Java Sprache zu erlernen. Während sich ein Java Entwickler also noch mit der Sprache herumschlägt, kann sich ein Smalltalk Entwickler bereits in die Klassenbibliothekt einarbeiten. Bzw. er muss, da in Smalltalk nun mal sämtliche Konstrukte wie IF-Statements und Loops in Form von Klassen und Methoden vorliegen. Ansonsten finden sich praktisch alle Klassen in Smalltalk auch in Java wieder oder umgekehrt, hier als Beispiel ein Auszug aus den Collections-Klassen:

SmalltalkJava
DictionaryMap
IdentityDictionaryHashMap
CollectionList
OrderedCollectionArrayList
Array(primitive Arrays)
SetSet
IdentitySetHashSet
usw.


Tools und IDE

In Smalltalk findet der grösste Teil der Entwicklung im Class-Browser statt. Im Gegensatz zu Java IDEs wie Eclipse, wo Editierung und Browsing getrennt sind, bildet dies in Smalltalk immer eine Einheit. Die Klassen werden nicht primär in einzelnen Dateien gespeichert sondern in einem gemeinsamen Image. Dies macht die Entwicklung mit Smalltalk sehr effizient.

Im Gegensatz dazu ist es in keiner mir bekannten Smalltalk Umgebung möglich, verschiedene Arten von Resourcen oder gar Sprachen über die gleiche IDE zu editieren.

Class-Browser in VisualWorks - Browsen und Editieren gleichzeitig

Smalltalk oder ...?

Die Sprache Smalltalk ist einfacher als Java aber trotzdem sehr mächtig ist und wirkt dank der konsequenten Umsetzung der Konzepte immer noch modern. Im direkten Vergleich zu Smalltalk (oder auch C#) wirkt Java (die Sprache) für mich eher schwerfällig und veraltet. Alternative Sprachen bringen zwar einige der Vorteile dynamischer Sprachen auch auf die Java Plattform, die IDE Unterstützung scheint mir aber bei Smalltalk immer noch einiges komfortabler zu sein.

OO-Konzepte, Exception-Handling und die Klassenbibliotheken sind ein weiten Teilen vergleichbar.
Das grösste Problem im Smalltalk Umfeld ist die sehr kleine Community, die eher beschränkten Resourcen (Entwickler) seitens der Smalltalk Hersteller und dass es kein wirklicher Standard gibt und nur sehr wenige Hersteller übergreifende Frameworks. Diese Punkte dürften schlussendlich doch für Java sprechen.

Es ist zu hoffen, dass sich noch einige der Ideen und Konzepte von Smalltalk in die Java oder andere Plattformen retten können und dann vielleicht irgendwann zu Mainstream-Technologien werden. Ich bin aber ziemlich sicher, dass dies noch einige Jahr(zehnt)e dauern wird.

http://www.cincomsmalltalk.com
http://www.instantiations.com/
http://www.object-arts.com/

Donnerstag, 8. November 2012

ObjectStudio 8.4 ListView extensions

Some years ago I implemented some ListView extensions using WindowProc subclassing and primitives. While testing the new ObjectStudio Release 8.4.1 I found that registering our own WindowProc lead to crashes. I originally used the CHookWnd class, but because this class was marked obsolete by its author I had to find another way.

Using CWindowImpl I was now able to create a first working demo again.

Here is the primitive method which I use to register my own WindowProc for a ListView. It gets called from Smalltalk right after the ListView opens:

 BOOL LV_Init() {

     INIT_LOCAL_GVARS;
    OPTR oSelf;
    oSelf = PTOSn(0);

    // Get CPP handle of ListView form item
    OPTR oCppHandle = GetObjVar(oSelf,GetInstVarOffset(oSelf,SymbolHash((PSZ)_T("cppHandle"))));
    if (IsNil(oCppHandle)) return FALSE;

    // Check if handle is valid and get a reference to the MFC CListCtrl object
    if(ObjHandle(oCppHandle) == NULL) return FALSE;
    if(!AfxIsValidAddress((void*)ObjHandle(oCppHandle),sizeof(CListCtrl), true)) 
                 return FALSE;
    CListCtrl* m_pCtrl = (CListCtrl*)ObjHandle(oCppHandle);
    if(!m_pCtrl) return FALSE;


    HWND hwnd = m_pCtrl->GetSafeHwnd();
    if (hwnd == NULL) return FALSE;

   // New Code to Subclass the WindowProc instead of CHookWnd
    CMyListViewEx *myListViewEx = new CMyListViewEx();
    BOOL result = myListViewEx->SubclassWindow(hwnd);


    return result;
}

And this is the extension demo class, printing out middle mouse buttons down events to the transcript:

class CMyListViewEx : public CWindowImpl<CMyListViewEx,CListCtrl>
{
public:

    BEGIN_MSG_MAP(
CMyListViewEx)
        MESSAGE_HANDLER(WM_MBUTTONDOWN, OnMButtonDown)
    END_MSG_MAP()

    LRESULT OnMButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {

       INIT_LOCAL_GVARS;
       Out(_T("MBUTTONDOWN %i %i %i"), uMsg, wParam, lParam);

       bHandled = TRUE;
       return TRUE;
    }

   
CMyListViewEx(void);
    ~
CMyListViewEx(void);
   
    void OnFinalMessage(HWND hWnd) {
        UnsubclassWindow(TRUE);
        delete this;
    }
}


If we ever finish porting from ObjectStudio 7 to 8 I may contribute our extensions to the Cincom public Store repository.

See also:

Donnerstag, 18. Oktober 2012

Xpert.Ivy Subversion Einstellungen

Ich hatte heute das Problem, dass bei einer neuen Installation vom Ivy Designer der SVN Benutzer und Passwort nicht gespeichert werden konnten und daher bei jedem SVN Zugriff wieder  Benutzer/Passwort abgefragt hat.

Subclipse (die in Xpert.Ivy verwendete Subversion Implementierung) speichert einige Daten im Workspace ab, einige liest es aus der Subversion Konfiguration im Benutzer Profil.

Die SVN-Benutzer-Angaben werden unter Windows im Benutzer Profil unter AppData gespeichert, zum Beispiel:

C:\Users\(User)\AppData\Roaming\Subversion\auth\svn.simple\

Nachdem wir die Datei in diesem Verzeichnis gelöscht haben, konnte Subclipse Benutzer/Passwort wieder speichern.

Die Daten im Workspace werden abgelegt unter:

workspace\.metadata\.plugins\org.tigris.subversion.subclipse.core\


Defaults von Ivy Workspace Einstellungen ändern

Nach der Installation vom Ivy Designer oder nach dem Erstellen eines neuen Workspaces musste ich bis jetzt immer einige der Einstellungen erneut anpassen, oder ich habe die Einstellung ganz vergessen. Daher habe ich heute nach Wegen gesucht, wie sich das vereinfachen lässst.

Die Ivy- und anderen Eclipse-Workspace Einstellungen sind im Workspace im folgendem Verzeichnis abgelegt:

(workspace)\.metadata\.plugins\org.eclipse.core.runtime\.settings

Erstellt man einen neuen Workspace, kann man diese Dateien in den neuen Workspace kopieren.

Möchte man die Einstellungen nicht jedesmal kopieren, kann man die Default Einstellungen auch in der folgenden Datei anpassen:

(XpertIvyDesigner)\plugins\ch.ivyteam.ivy.designer.branding_4.3.0\plugin_customization.ini

Hier einige Beispiele für diese Datei:

ch.ivyteam.ivy.designer.ide/VISIBILITY_LEVEL=EXPERT
ch.ivyteam.ivy.designer.ide/ANIMATION_FOLLOWING_MODE=FOLLOW_ONLY_OPEN_PROCESSES
org.eclipse.core.resources/encoding=UTF-8
org.eclipse.ve.java.core/SWING_LOOKANDFEEL=my.look-and-feel


Das Format der Einträge in dieser Datei ist:

plugin-name/configuration=value

Wie die Einstellungen heissen findet man am einfachsten heraus indem man die Settings-Dateien im Workspace (.metadata\.plugins\org.eclipse.core.runtime\.setting) in einem Text-Editor öffnet. Sind dort noch keine vorhanden, kann man die Einstellung im Ivy Designer ändern und dann den Designer beenden, dann sollten die Einstellungen in die Dateien geschrieben werden.

Der Plugin-Name entspricht normalerweise der Settings-Datei ohne die Endung "prefs". Man kann den Plugin-Namen aber auch verifizieren wenn man im Designer die Einstellungsseite öffnet und dann Alt-Shift-F1 drückt (Plugin Spy).

Als Beispiel hier den Inhalt der Datei ch.ivyteam.ivy.designer.ide.prefs im:

#Thu Oct 18 13:32:13 CEST 2012
VISIBILITY_LEVEL=EXPERT

ANIMATION_FOLLOWING_MODE=FOLLOW_ONLY_OPEN_PROCESSES
eclipse.preferences.version=1


Es sollte auch möglich sein eine eigene plugin_configuration.ini Datei über die Commandline mittels -pluginCustomization anzugeben.

Einige Einstellungen lassen sich auch direkt über die Commandline ändern, z.Bsp. das Encoding:

-Dfile.encoding=UTF-8

Die Commandline Argumente können in der Datei "Xpert.Ivy Designer.ini" auf einer neuen Zeile eingefügt werden:

-startup
plugins/org.eclipse.equinox.launcher_1.0.201.R35x_v20090715.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_1.0.200.v20090519
-nl
en
-vmargs
-Xms128m
-Xmx768m
-XX:MaxPermSize=192m
-Dosgi.parentClassloader=ext
-da
-Dsun.java.command=XpertIvyDesigner
-Dfile.encoding=UTF-8


Daneben  gibt es noch die Möglichkeit über das Export/Import-Menu Einstellungen zu exportieren/importieren.

Allerdings konnte ich bis jetzt nicht alle Einstellungen mit oben genanntem Weg übernehmen, z.Bsp. muss ich die SVN-Repositories (workspace\.metadata\.plugins\org.tigris.subversion.subclipse.core\.svnProviderState) immer noch von Hand kopieren oder erfassen.

Links

Donnerstag, 27. September 2012

System Integration mit Xpert.Ivy: Event Start über Dateien

Xpert.Ivy bringt von Haus aus bereits Möglichkeiten mit, andere Systeme über eine Datei basierte asynchrone Kommunikation in Prozesse einzubinden. Die Datei basierte Integration hat den Vorteil, dass sie einfach und ohne zusätzliche Software umsetzbar ist. In einem Prozess Showcase, bei dem drei verschiedene Systeme integriert wurden, haben wir daher auf diese Variante gesetzt für die asynchrone Kommunikation.


Wenn man einen Prozess in Xpert.Ivy durch eine Datei starten oder weiterlaufen lassen möchte, kann man dies über ein "Event Start" oder ein "Intermediate Event" realisieren.


Schauen wir zuerst das Event "Event Start" an. Xpert.Ivy wird bereits mit drei Java Klassen (auch StartEvenBeans genannt) ausgeliefert.


Im Projekt IvyAddOns gibt es darüber hinaus noch weitere EventBeans. Um diese zu finden wechselt man am besten in die Java Perspektive (Menu Window => Open Perspective => Other => Java). Wenn man nun das Projekt IvyAddOns aufmacht, findet man im Ordner src zwei Packages mit weiteren Start- und Intermediate-EventBeans:


Die Klasse XMLFileStartEventBean ist dabei ein guter Startpunkt, wenn man eine eigene Klasse implementieren möchte. Ich werde später ein Beispiel dazu zeigen.



Hier eine kurze Beschreibung der StartEventBeans.

FileStartEventBean

Startet für jede Datei in einem bestimmten Verzeichnis ein Prozess und kopiert die Werte in der Datei in die Prozessdaten. Die Datei muss dafür folgendes Format haben:

Attribute1=Wert1,Attribute2=Wert2,...

Die Pollzeit ist fest auf eine Minute eingestellt. Die Datei wird gelöscht. Attribute die nicht übereinstimmen werden ignoriert.

Beispiel mit Debug Ausgabe der Prozess Attribute:

FilePickupStartEventBean

Startet für jede Datei in einem bestimmten Verzeichnis ein Prozess und kopiert den Dateinamen in ein Prozess-Attribut vom Typ String.

Die Pollzeit ist fest auf eine Minute eingestellt. Die Datei wird wenn das Setzen des Dateinamens funktioniert gelöscht, im Fehlerfall stehen gelassen.

Beispiel mit Debug Ausgabe:

XMLFileStartEventBean (aus IvyAddOns)

Startet für jede Datei in einem Verzeichnis ein Prozess und kopiert die XML Elemente in die Prozess Attribute (analog FileStartEventBean aber mit einer XML Datei).

Die Pollzeit kann eingestellt werden. Das Verzeichniss kann aus den globalen Variablen von Ivy gelesen werden. Im Fehlerfall wird die Datei in ein Verzeichnis "error" verschoben.

Beispiel:




Fazit

Mit den in Ivy integrierten EventStartBeans ist es möglich, schnell eine Datei basierte Kommunikation zwischen dem Prozess und verschiedenen Systemen umzusetzen. Allerdings sind sie für einen produktiven Einsatz nur bedingt brauchbar, da:
  • Die Pollzeit (ausser bei XMLFileStartEventBean) nicht konfigurierbar ist
  • Das Verzeichnis (ausser bei XMLFileStartEventBean) nicht über eine globale Ivy Variable konfigurierbar ist
  • Das Fehler-Handling zu einfach ist
  • Fehler beim gleichzeitigen Schreiben (vom externen System) und Lesen (durch Ivy) nicht ausgeschlossen sind
  • Kein Datei-Filter angewendet werden kann
Wie man selber eine StartEventBean schreiben kann, welche diese Punkte umsetzt, zeige ich in einem späteren Post.

Mittwoch, 26. September 2012

Xpert.Ivy HTML UI based on JSF?

Looks like Soreco decided to take JSF to replace the JSP based HTML dialogs in Xpert.Ivy:

http://www.ivyteam.ch/flatpress/?entry=entry120917-133839

That would be interessting for us, because we already have some JSF knowledge.

http://fzkvision.ch/

SQLServer Plan Info

I was debugging today a SQL statement which took about 40 seconds in one app, but only 200 ms in SSMS. I learned that SQLServer creates different execution plans depending on the SET options.

Here is a SQL statement to print out some infos about cached execution plans for a specific statement:

SELECT
    query_info.sql_handle,
    query_info.plan_handle,
    plan_options.usecounts,
    query_info.total_elapsed_time,
    CASE WHEN ((1 & set_options) = 1) THEN 'ON' ELSE 'OFF' END AS 'ANSI_PADDING',
    CASE WHEN ((4 & set_options) = 4) THEN 'ON' ELSE 'OFF' END AS 'FORCEPLAN',
    CASE WHEN ((8 & set_options) = 8) THEN 'ON' ELSE 'OFF' END AS 'CONCAT_NULL_YIELDS_NULL',
    CASE WHEN ((16 & set_options) = 16) THEN 'ON' ELSE 'OFF' END AS 'ANSI_WARNINGS',
    CASE WHEN ((32 & set_options) = 32) THEN 'ON' ELSE 'OFF' END AS 'ANSI_NULLS',
    CASE WHEN ((64 & set_options) = 64) THEN 'ON' ELSE 'OFF' END AS 'QUOTED_IDENTIFIER',
    CASE WHEN ((128 & set_options) = 128) THEN 'ON' ELSE 'OFF' END AS 'ANSI_NULL_DFLT_ON',
    CASE WHEN ((256 & set_options) = 256) THEN 'ON' ELSE 'OFF' END AS 'ANSI_NULL_DFLT_OFF',
    CASE WHEN ((512 & set_options) = 512) THEN 'ON' ELSE 'OFF' END AS 'NoBrowseTable',
    CASE WHEN ((4096 & set_options) = 4096) THEN 'ON' ELSE 'OFF' END AS 'ARITH_ABORT',
    CASE WHEN ((8192 & set_options) = 8192) THEN 'ON' ELSE 'OFF' END AS 'NUMERIC_ROUNDABORT',
    CASE WHEN ((16384 & set_options) = 16384) THEN 'ON' ELSE 'OFF' END AS 'DATEFIRST',
    CASE WHEN ((32768 & set_options) = 32768) THEN 'ON' ELSE 'OFF' END AS 'DATEFORMAT',
    CASE WHEN ((65536 & set_options) = 65536) THEN 'ON' ELSE 'OFF' END AS 'LanguageID',
    query_info.text,
    plan_info.query_plan
FROM (
    SELECT plan_handle, usecounts, CAST(pvt.set_options AS INT) AS set_options
    FROM (
        SELECT plan_handle, usecounts, epa.attribute, epa.value
        FROM sys.dm_exec_cached_plans
            OUTER APPLY sys.dm_exec_plan_attributes(plan_handle) AS epa
        WHERE cacheobjtype = 'Compiled Plan') AS ecpa
    PIVOT (MAX(ecpa.value) FOR ecpa.attribute IN ("set_options", "objectid")) AS pvt
) as plan_options
JOIN (
    SELECT qs.sql_handle AS sql_handle, qs.plan_handle AS plan_handle, qs.total_elapsed_time, st.text
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(sql_handle) st
) AS query_info ON query_info.plan_handle = plan_options.plan_handle
CROSS APPLY  sys.dm_exec_query_plan(query_info.plan_handle) plan_info
WHERE text LIKE 'SELECT ...%';
 
Thanks to:

Generate SQLServer Scripts with PowerShell

I needed a new script to generate SQL scripts for tables, views etc. The old script did export all tables as files with extension "TAB", all views with extension "VIW", etc. I wanted to keep this, but needed all the files in correct order for dependencies. So here we go: my first PowerShell script.

In order to connect to a SQLServer we must first load some assemblies:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | out-null
[System.Reflection.Assembly]::LoadWithPartialName("System.Data") | out-null

Now it is possible to connect to the server:

$srv = new-object "Microsoft.SqlServer.Management.SMO.Server" "NAME_OF_SERVER"
$srv.ConnectionContext.LoginSecure = $false
$srv.ConnectionContext.Login = "USER_NAME"
$srv.ConnectionContext.Password ="PASSWORD"


Get a reference to the database:

$dbName = "DATABASE_NAME"
$db = New-Object "Microsoft.SqlServer.Management.SMO.Database"
$db = $srv.Databases[$dbName]


Get a scripter instance and set options:

$scripter = New-Object "Microsoft.SqlServer.Management.Smo.Scripter"
$scripter.Server = $srv

$scrOpts = New-Object "Microsoft.SqlServer.Management.SMO.ScriptingOptions"
$scrOpts.Encoding = [System.Text.Encoding]::GetEncoding(1252)
$scrOpts.AllowSystemObjects = $false
$scrOpts.AnsiFile = $true
$scrOpts.AppendToFile = $false
$scrOpts.ClusteredIndexes = $true
$scrOpts.Default = $true
$scrOpts.DriAll = $true
$scrOpts.Indexes = $true
$scrOpts.NonClusteredIndexes = $true
$scrOpts.ToFileOnly = $true
$scrOpts.WithDependencies = $false
$scrOpts.Triggers = $true
$scripter.Options = $scrOpts


See MSDN for all Options .

Export all tables:

# Read all tables into an array, filter out system tables
$Objects = @()

$Objects += $db.Tables | where {$_.IsSystemObject -eq $false}
# Find dependencies
$dependencyTree = $scripter.DiscoverDependencies($Objects, $true)
# Create a list of all needed objects in correct order
$depCollection = $scripter.WalkDependencies($dependencyTree);


Unfortunatelly this did nor work the first time, because there was a trigger on a table which called a stored procedure. DiscoverDependencies also returned this procedure and all views and other objects which this procedure was dependent of. Filtering out the procedure helped. To get this done one can set a callback function which gets called by DiscoverDependencies:

function ScriptingFilter([Microsoft.SqlServer.Management.Sdk.Sfc.Urn]$urn) {
    $item = $srv.GetSmoObject($urn)
    if($item -is [Microsoft.SqlServer.Management.Smo.StoredProcedure])
    {
        Write-Host "  Filter: " $item " => " $type
        return $true
    } else {
        return $false
    }
}


$scripter.FilterCallbackFunction = get-content Function:\ScriptingFilter


Now create a script file for each object:

# Create a UrnCollection of all objects
$urns = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
foreach($dep in $depCollection) { $urns.add($dep.Urn) }


$onlyOne = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
$i = 0
$max = $urnCollection.Count

foreach($urn in $urnCollection)
{
    $onlyOne.clear()
    $item = $srv.GetSmoObject($urn)
    $ext = "SQL"
    if($
item -is [Microsoft.SqlServer.Management.Smo.Table])
    {
        $ext = "TAB"
    }
    if($
item -is [Microsoft.SqlServer.Management.Smo.View])
    {
        $ext = "VIW"
    }
    if($
item -is [Microsoft.SqlServer.Management.Smo.StoredProcedure])
    {
        $ext = "PRC"
    }
    if($
item -is [Microsoft.SqlServer.Management.Smo.UserDefinedFunction])
    {
        $ext = "UDF"
    }
    if($
item -is [Microsoft.SqlServer.Management.Smo.UserDefinedDataType])
    {
        $ext = "UDT"
    }



    # Filter out some more objects we do not need     if($item.Name.StartsWith("spt_") -or $item.IsSystemObject -eq $true)
    {
        Write-Host " Ignored: " $item.Name
     } else {
            $count = $count + 1
            $scripter.Options.FileName = $scriptPath + $count.ToString("0000") + '.' + $item.Schema + "." + $item.Name + "." + $ext
            $scripter.Script($onlyOne)
            $i = $i + 1
    }
}


Finally I added some status information using Write-Progress commandlet and created some functions so that it was possible to script first all tables, then all views and procedures.

Some helpfull links:

The full script:

#Set-ExecutionPolicy RemoteSigned

#
# Extension fuer ein bestimmtes SmoObject ermitteln
#
function GetTypeExt([Microsoft.SqlServer.Management.Smo.SqlSmoObject] $Item)
{
    $ext = "SQL"
    if($Item -is [Microsoft.SqlServer.Management.Smo.Table])
    {
        $ext = "TAB"
    }
    if($Item -is [Microsoft.SqlServer.Management.Smo.View])
    {
        $ext = "VIW"
    }
    if($Item -is [Microsoft.SqlServer.Management.Smo.StoredProcedure])
    {
        $ext = "PRC"
    }
    if($Item -is [Microsoft.SqlServer.Management.Smo.UserDefinedFunction])
    {
        $ext = "UDF"
    }
    if($Item -is [Microsoft.SqlServer.Management.Smo.UserDefinedDataType])
    {
        $ext = "UDT"
    }
    return $ext
}

#
# Function um bestimmte Elemente aus den Abhaengigkeiten auszuschliessen
#
function ScriptingFilter([Microsoft.SqlServer.Management.Sdk.Sfc.Urn]$urn) {
    $item = $srv.GetSmoObject($urn)
    $type = GetTypeExt($item)
    if(-not $Filter -eq "" -and $type.StartsWith($Filter))
    {
        Write-Host "  Filter: " $item " => " $type
        return $true
    } else {
        return $false
    }
}

#
# Generiert Scripts ohne Abhaengigkeiten fuer Objekte in $Objects
#
function GenScripts()
{
    $urns = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
    foreach($dep in $Objects) { $urns.add($dep.Urn) }
    GenScriptsForObjects $urns
}

#
# Generiert Scripts mit Abhaengigkeiten fuer Objekte in $Objects
#
function GenScriptsWithDependencies()
{
    $scripter.FilterCallbackFunction = get-content Function:\ScriptingFilter
    if($Objects.Length -ne 0)
    {
        $dependencyTree = $scripter.DiscoverDependencies($Objects, $true)
        $depCollection = $scripter.WalkDependencies($dependencyTree);
        $urns = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
        foreach($dep in $depCollection) { $urns.add($dep.Urn) }
        GenScriptsForObjects $urns
    }
}

#
# Generiert Scripts fuer eine Collection von SmoObjects
#
function GenScriptsForObjects([Microsoft.SqlServer.Management.Smo.UrnCollection]$urnCollection)
{
        $onlyOne = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
        $i = 0
        $max = $urnCollection.Count

        foreach($urn in $urnCollection)
        {
            $onlyOne.clear()
            $item = $srv.GetSmoObject($urn)
           
            Write-Progress -Activity "Script objects..." -status $item.Name -percentComplete ($i / $max * 100)
           
            $ext = GetTypeExt($item)
           
            if($item.Name.StartsWith("spt_") -or $item.IsSystemObject -eq $true)
            {
                Write-Host "  Ignored: " $item.Name
            } else {
                if(-not $UrnsDone.Contains($item.Urn))
                {
                    $onlyOne.add($item.Urn)
                    $global:count = $UrnsDone.Count + 1
                    $scripter.Options.FileName = $scriptPath + $count.ToString("0000") + '.' + $item.Schema + "." + $item.Name + "." + $ext
                    $scripter.Script($onlyOne)
                    $UrnsDone.add($item.Urn)
                    $i = $i + 1
                }
            }
        }
}

#
# Hauptfunktion: Stellt Verbindung mit SQLServer her, ermittelt die Objekte und Scripted diese
#
function GenerateDBScript([string]$dbServer, [string]$dbName, [string]$dbUser, [string]$dbPsw, [string]$scriptPath)
{
    if ($scriptPath[-1] -ne "\")
    {
        $scriptPath = $scriptPath + "\"
    }
    if (!(Test-Path -path $scriptPath))
    {
       New-Item $scriptPath -type directory
    }
    if (Test-Path -path ($scriptPath + "0*.*"))
    {
        $msg = "Verzeichnis """ + $scriptPath + """ enthält bereits Script-Dateien. Zuerst alle Dateien löschen."
        [System.Windows.Forms.MessageBox]::Show($msg) | out-null
        $msg
        explorer $scriptPath
    } else {
        "Generate scripts for server: " + $dbServer + " database: " + $dbName
        "  to Path: " + $scriptPath

        [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | out-null
        [System.Reflection.Assembly]::LoadWithPartialName("System.Data") | out-null

        $srv = new-object "Microsoft.SqlServer.Management.SMO.Server" $dbServer
        $srv.ConnectionContext.LoginSecure = $false
        $srv.ConnectionContext.Login = $dbUser
        $srv.ConnectionContext.Password = $dbPsw
        $srv.SetDefaultInitFields([Microsoft.SqlServer.Management.SMO.View], "IsSystemObject")

        $db = New-Object "Microsoft.SqlServer.Management.SMO.Database"
        $db = $srv.Databases[$dbName]

        $scripter = New-Object "Microsoft.SqlServer.Management.Smo.Scripter"
        $scripter.Server = $srv

        $scrOpts = New-Object "Microsoft.SqlServer.Management.SMO.ScriptingOptions"
        #$scrOpts.Encoding = [System.Text.Encoding]::GetEncoding(1252)
        $scrOpts.AllowSystemObjects = $false
        $scrOpts.AnsiFile = $true
        $scrOpts.AppendToFile = $false
        $scrOpts.ClusteredIndexes = $true
        $scrOpts.Default = $true
        $scrOpts.DriAll = $true
        $scrOpts.Indexes = $true
        $scrOpts.NonClusteredIndexes = $true
        $scrOpts.ToFileOnly = $true
        $scrOpts.WithDependencies = $false
        $scrOpts.Triggers = $true
        $scripter.Options = $scrOpts

        $UrnsDone = New-Object Microsoft.SqlServer.Management.Smo.UrnCollection;
        $Filter = ""
        $global:count = 0

        try {

            $Objects = @()
           
            Write-Progress -Activity "Script Objects..." -status "User Defined Data Types" -percentComplete 0
            $Res = $db.UserDefinedDataTypes | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }
       
            Write-Progress -Activity "Script Objects..." -status "Tables" -percentComplete 0
            $Res = $db.Tables | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }

            $Filter = "PRC"
            GenScriptsWithDependencies
           
           
            $Objects = @()
       
            Write-Progress -Activity "Script Objects..." -status "User Defined Functions" -percentComplete 0
            $Res = $db.UserDefinedFunctions | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }

            Write-Progress -Activity "Script Objects..." -status "Views" -percentComplete 0
            $Res = $db.Views | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }

            $Filter = "PRC"
            GenScriptsWithDependencies

           
            $Objects = @()
           
            Write-Progress -Activity "Script Objects..." -status "DB Triggers" -percentComplete 0
            $Res = $db.Triggers | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }

            Write-Progress -Activity "Script Objects..." -status "Procedures" -percentComplete 0
            $Res = $db.StoredProcedures | where {$_.IsSystemObject -eq $false}
            if($Res)
            {
                $Objects += $Res
            }

            $Filter = ""
            GenScriptsWithDependencies

        } catch {
            "EXCEPTION: " + $_.Exception.GetBaseException().Message
            throw $_.Exception
        }

        "Done. " + $count + " Dateien erstellt in " + $scriptPath
        explorer $scriptPath
    }
}

# GenerateDBScript $args[0] $args[1] $args[2] $args[3] $args[4]
GenerateDBScript "SERVER" "DATABASE" "USER" "PASSWORD" "DIRECTORY"