Griffon Das Tutorial Griffon im praktischen Einsatz Dem Greifen Leben einhauchen Quellcode auf CD! Nach der Einführung in die Konzepte von Griffon werden wir in diesem Teil Quellcode sehen und mit der Entwicklung beginnen. Dabei bekommen Sie Tipps für die Arbeit mit Griffon [1] sowie Lösungen zu bekannten Problemen. von Alexander Klein Bevor wir loslegen, soll zur Erinnerung die Problemstellung unseres Projekts Babylonier wiederholt werden. I18n-Unterstützung Zur Entwicklungsunterstützung von internationalisierten Anwendungen wird folgender Prozess umgesetzt: Während der Entwicklung werden.properties-dateien mit Key-Value-Paaren für Instanzen von java.util. ResourceBundle verwendet. Die Entwickler pflegen ausschließlich die.properties- Datei ohne Locale-Informationen, die im Weiteren als Default-Sprache bezeichnet wird. Eine Sprache wird als Basissprache für die Übersetzungen definiert und über einen redaktionellen Prozess aus der Default-Sprache erstellt und gepflegt. Die Basissprache wird an das Übersetzungsbüro übergeben und die übersetzten Texte werden parallel zu den Dateien der Default-Sprache und der Basissprache im Anwendungsquellcode abgelegt. Es muss überwacht werden, ob alle Texte in alle Sprachen übersetzt wurden, ob sich ein Text in der Default- Sprache geändert hat und neu übersetzt werden muss, oder gar ein Key aus der Anwendung gelöscht wurde. Für diesen Prozess muss unser Tool folgendes erfüllen: Quellpfad rekursiv nach den Dateien der Default- Sprachen von ResourceBundles durchsuchen und alle Key-Value-Paare mitsamt ihrem Ursprung in einer Datenbank ablegen. Dazu den Key-Status speichern: Key angelegt, verändert oder gelöscht Erfassen bzw. Ändern der Texte der Basissprache für die eingelesenen Keys Exportieren der Texte einer Sprache für das Übersetzungsbüro Importieren der übersetzten Texte für die jeweilige Sprache Erzeugen der sprachspezifischen ResourceBundle- Dateien an der richtigen Stelle im Quellpfad Das Layout Babylonier soll eine ordentliche Anwendung werden, deshalb betten wir es in ein gängiges Layout (Abb. 1, links: eine Menüzeile und Statusleiste, ein Navigationsbereich links und eine Fläche für die einzelnen Funktionen. Wenn externe Bibliotheken wie zum beispiel MiGLayout [2] als LayoutManager verwendet werden sollen, gibt es in Griffon drei Möglichkeiten, diese einzubinden: Man legt die.jar-datei in den /lib-ordner im Projektverzeichnis, dieses wird automatisch in den Classpath aufgenommen. Wenn die Bibliothek in einem Maven Repository zur Verfügung steht, kann sie als Abhängigkeit in griffon-app/config/buildconfig. unter griffon.project.dependency.resolution.dependencies eingetragen werden. Für einige Bibliotheken existieren Plug-ins [3]. Sie liefern nicht nur die.jar-datei mit, sondern bieten oft zusätzliche Funktionen. Mit griffon list-plugins kann man sehen, welche Plug-ins installierbar und welche installiert sind. Auf der Griffon- Webseite gibt es eine aufbereitete Liste mit weitergehenden Informationen. Plug-ins Für MiGLayout existiert ein Plug-in, das einen Builder- Knoten liefert und das wir mit dem Kommando griffon www.jaxenter.de javamagazin 9 2011 79
Das Tutorial Griffon install-plugin miglayout installieren. Weil das Menü und die Navigation mit Icons ausgestattet werden, installieren wir mit griffon install-plugin crystalicons Everaldos Listing 1: griffon-app/model/babylonianmodel. import.beans.bindable class BabylonianModel { @Bindable String currentelement = ' ' @Bindable String status Listing 2: griffon-app/views/babylonianview. build(babylonianactions application(title: 'Babylonian', name: 'MainFrame', // for shutdownhook size: [1000, 750], //pack: true //location: [50,50], locationbyplatform: true, iconimage: imageicon('/griffon-icon-48x48.png'.image, iconimages: [imageicon('/griffon-icon-48x48.png'.image, imageicon('/griffon-icon-32x32.png'.image, imageicon('/griffon-icon-16x16.png'.image] { menubar(build(babylonianmenu miglayout(layoutconstraints: 'fill' buttongroup(id: 'group' panel(id: 'navigation', constraints: 'dock west, width 150' { miglayout(layoutconstraints: 'wrap 1, fill', columnconstraints: 'fill', rowconstraints: 'fill' togglebutton(id: 'overviewbutton', action: overviewaction, buttongroup: group togglebutton(id: 'scanbutton', action: scanaction, buttongroup: group togglebutton(id: 'editbutton', action: editaction, buttongroup: group togglebutton(id: 'generatebutton', action: generateaction, buttongroup: group togglebutton(id: 'exportbutton', action: exportaction, buttongroup: group togglebutton(id: 'importbutton', action: importaction, buttongroup: group togglebutton(id: 'configbutton', action: configaction, buttongroup: group panel(id: 'content', constraints: 'dock center' { cardlayout( widget(app.views.overview.mainpanel, constraints: 'overview' widget(app.views.scan.mainpanel, constraints: 'scan' widget(app.views.edit.mainpanel, constraints: 'edit' widget(app.views.export.mainpanel, constraints: 'export' widget(app.views.import.mainpanel, constraints: 'import' widget(app.views.generate.mainpanel, constraints: 'generate' widget(app.views.config.mainpanel, constraints: 'config' controller.navigate('overview' panel(build(babylonianstatusbar, constraints: 'dock south' Crystal Icons Set [4]. Plug-ins können nicht nur Bibliotheken und Ressourcen liefern. Sie können Griffon sowohl zur Entwicklungszeit als auch zur Laufzeit erweitern: Skripte können zur Entwicklungszeit genutzt werden und erleichtern die Entwicklung (z. B. griffon crystalicon-selector öffnet ein Vorschaufenster zur Icon- Auswahl Builder und Builder-Knoten erweitern die Möglichkeiten, Views zu erstellen Plug-ins können neue Artefakte liefern (z. B. nutzt das Spock-Plug-in ein Artefakt für Test-Specs Sie können zusätzliche Funktionen wie einen Ladebildschirm oder neue Komponenten bieten Der Rahmen Nachdem wir im letzten Artikel die ModelViewCon troller-gruppe (im Weiteren MVCGroup genannt schon verwendet haben, ersetzen wir sie durch den Code in Listing 1 bis 3. BabylonianModel enthält nur zwei Eigenschaften vom Typ String, um eine Statusmeldung und die aktuelle Position in der Navigation anzuzeigen. Um die BabylonianView überschaubar zu halten, teilen wir die Oberfläche in mehrere Dateien auf und lösen die Erstellung der Actions (BabylonianActions, Listing 4, des Menüs (BabylonianMenu, Listing 5 und der Statuszeile (BabylonianStatusBar, Listing 6 heraus. Listing 3: griffon-app/controllers/ BabylonianController. import java.awt.event.windowevent import griffon.transform.threading class BabylonianController { // these will be injected by Griffon def model def view private def oldmvc // Navigation actions @Threading(Threading.Policy.SKIP def navigate = { mvcname -> model.currentelement = view."${mvcnamebutton".text view.group.setselected(view."${mvcnamebutton".model, true view.content.layout.show(view.content, mvcname app.event('navigation', [mvcname, oldmvc] oldmvc = mvcname // Menu actions def exit = { evt = null -> def wnd = app.windowmanager.findwindow('mainframe' wnd.processwindowevent(new WindowEvent(wnd, WindowEvent. WINDOW_CLOSING def about = { evt = null -> 80 javamagazin 9 2011 www.jaxenter.de
Griffon Das Tutorial Um auf die Actions im Menü und in der Navigation verweisen zu können, müssen sie zuerst erstellt werden. Das geschieht mit build(babylonianactions, wobei dieselbe Builder-Instanz wie bei BabylonianView für die Erstellung verwendet wird. Somit sind alle nicht lokalen Variablen und Komponenten mit id-attribut im gesamten Kontext des Builders verfügbar. BabylonianActions ist eine Sammlung von Actions, wie sie bei SwingBuilder [5] beschrieben ist. Die Attribute smallicon und swinglargeiconkey definieren die Icons in der Menüzeile und in den Navigationsbuttons. Das Crystal-Icon-Plug-in bietet uns mit dem Knoten crystalicon eine bequeme Möglichkeit, ein Icon aus der Bibliothek zu verwenden. Die Actions für Exit und About führen die gleichnamigen Closures in BabylonianControl ler aus. Alle anderen rufen die Methode navigate mit dem Namen der anzuzeigenden MVCGroup auf. Gehen wir zurück zu BabylonianView. Weil wir später das Applikationsfenster aus dem Code referenzieren müssen, bekommt der application-knoten den Namen MainFrame, dessen Größe wir auf 1000 x 750 definieren. Mit menubar(build(babylonianmenu binden wir die JMenuBar-Instanz, die das Script mit unserer Abb. 1: Layout für unseren Rahmen: Schema und Ergebnis Menüdefinition zurückliefert, an unser Fenster. Wenn ein SwingBuilder-Knoten eine Instanz als Parameter übergeben bekommt, wird keine neue erzeugt, sondern die übergebene Instanz an dieser Stelle verwendet. Die Menüleiste in BabylonianMenu besteht aus zwei Menüs: File und Help. Help hat ein JMenuItem, About, und wird mit glue( rechtsbündig ausgerichtet. Im File- Menü spiegeln wir die Einträge der Navigation und fügen den Punkt Exit hinzu (außer Mac-OS-X-System. Hier ist zu sehen, dass ein View-Skript ein normaler, ausführbarer Code ist und somit dynamisch generiert werden kann. Indem wir die Actions referenzieren, wer- Listing 4: griffon-app/views/babylonianactions. actions { action(id: 'overviewaction', name: 'Overview', shortdescription: 'Overview', mnemonic: 'O', accelerator: shortcut('o', smallicon: crystalicon(icon: 'agt_web', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'agt_web', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('overview' action(id: 'scanaction', name: 'Scan', shortdescription: 'Scan for unlocalized properties', mnemonic: 'S', accelerator: shortcut('s', smallicon: crystalicon(icon: 'filefind', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'filefind', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('scan' action(id: 'editaction', name: 'Edit', shortdescription: 'Edit translations', mnemonic: 'E', accelerator: shortcut('e', smallicon: crystalicon(icon: 'edit', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'edit', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('edit' action(id: 'generateaction', name: 'Generate', shortdescription: 'Generate localized properties', mnemonic: 'G', accelerator: shortcut('g', smallicon: crystalicon(icon: 'gear', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'gear', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('generate' action(id: 'exportaction', name: 'Export', shortdescription: 'Export keys for translation', mnemonic: 'x', accelerator: shortcut('ctrl E', smallicon: crystalicon(icon: 'download', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'download', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('export' action(id: 'importaction', name: 'Import', mnemonic: 'I', accelerator: shortcut('ctrl I', shortdescription: 'Import translated keys', smallicon: crystalicon(icon: 'folder_sent_mail', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'folder_sent_mail', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('import' action(id: 'configaction', name: 'Configure', shortdescription: 'Configure', mnemonic: 'C', accelerator: shortcut('ctrl f', smallicon: crystalicon(icon: 'configure', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'configure', size: 32, category: 'actions', closure: { evt = null -> controller.navigate('config' action(id: 'exitaction', name: 'Exit', shortdescription: 'Exit this application', mnemonic: 'x', accelerator: shortcut('ctrl X', smallicon: crystalicon(icon: 'exit', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'exit', size: 32, category: 'actions', closure: controller.exit action(id: 'aboutaction', name: 'About', shortdescription: 'Informations about this application', mnemonic: 'A', accelerator: shortcut('a', smallicon: crystalicon(icon: 'documentinfo', size: 16, category: 'actions', swinglargeiconkey: crystalicon(icon: 'documentinfo', size: 32, category: 'actions', closure: controller.about www.jaxenter.de javamagazin 9 2011 81
Das Tutorial Griffon Listing 5: griffon-app/views/babylonianmenu. import static griffon.util.griffonapplicationutils.isismacosx menubar { menu(text: 'File', mnemonic: 'F' { menuitem(overviewaction menuitem(scanaction menuitem(editaction menuitem(exportaction menuitem(importaction menuitem(generateaction menuitem(configaction if (!ismacosx { separator( menuitem(exitaction glue( menu(text: 'Help', mnemonic: 'H' { menuitem(aboutaction Listing 6: griffon-app/views/babylonianstatusbar. panel(id: 'statuspanel' { miglayout(layoutconstraints: 'fill', columnconstraints: '5[150][fill, right]' label(id: 'status', text: bind { model.status, horizontalalignment: SwingConstants.LEADING label(id: 'currentelement', text: bind { model.currentelement, horizontalalignment: SwingConstants.TRAILING Listing 7: griffon-app/views/overviewview. und alle anderen neuen Views scrollpane(id: 'mainpanel' { panel( { den die Informationen wie Text und Icon automatisch übernommen. Zurück in BabylonianView sehen wir ein JPanel mit unserer Navigation: eine ButtonGroup mit JToggleButtons für jeden Eintrag. Auch hier werden alle anzuzeigenden Informationen aus den Actions übernommen. Für den Hauptbereich verwenden wir ebenfalls ein JPanel, das CardLayout als LayoutManager bekommt. Das entspricht einer JTabbedPanel ohne Reiter, denn diese Funktion soll unsere Navigation bzw. das Menü übernehmen. Als Inhalt der einzelnen, reiterlosen Seiten fügen wir eine Komponente mit dem Namen mainpanel aus der entsprechenden View der MVCGroup hinzu. Leider ist es uns nicht unbedingt bekannt, welchen Typ mainpanel hat. Deshalb verwenden wir den Knoten widget, der zwar keine neue Instanz erzeugen, dafür aber alle Objekte vom Typ java.awt.component aufnehmen kann. Zudem existiert noch der Knoten container, der im Gegensatz zu widget andere verschachtelte Knoten aufnehmen kann. Der Inhalt Da wir noch gar keine MVCGroups für die einzelnen Funktionen haben, müssen wir die MVCGroups overview, scan, edit, generate, import, export und config mit griffon create-mvc overview etc. anlegen. Alle View- Skripte dieser MVCGroups sollen erst einmal wie Listing 7 aussehen und sind leere JScrollPanes. Zur Laufzeit müssen die MVCGroups instantiiert werden. Das kann man dynamisch mit dem Befehl buildmvcgroup(string groupname tun. Weil wir aber die MVCGroups beim Start instantiieren müssen, können wir das auch Griffon selbst erledigen lassen. In griffon-app/conf/application. gibt es unter application den Eintrag startgroups. Hier tragen wir MVCGroups ein, die beim Start instantiiert werden sollen. In unserem Fall also Folgendes: startupgroups = ['overview', 'scan', 'edit', 'generate', 'import', 'export', 'config', 'babylonian'] Bevor nun die Statusleiste aus Listing 6 eingebettet wird, müssen wir sicherstellen, dass beim Start die MVC- Group overview angezeigt wird. Dazu führen wir die navigate-methode programmatisch aus. Die Logik In BabylonianController steht unsere Logik, die über die Navigation und das Menü aufgerufen wird. Die Closure exit simuliert ein reguläres Schließen über den Schliessen-Button des Fensters, wobei wir über den WindowManager an das Fenster mit dem vorher vergebenen Namen MainFrame gelangen. Der Menüpunkt Help About soll hier nicht ausprogrammiert werden, deshalb schreiben wir nur eine leere Closure about. Alle anderen Aktionen rufen navigate auf. Wie im letzten Artikel schon erwähnt, werden alle Controller-Methoden standardmäßig außerhalb des Event Dispatch Threads aufgerufen. Dazu wird ein eigener Thread gestartet. Der Aufruf von navigate sollte das aber nicht tun, um Threads und Ressourcen zu sparen. Deshalb wird das Thread- Handling für diese Methode mit der Annotation @Threading(Threading.Policy.SKIP ausgeschaltet. 82 javamagazin 9 2011 www.jaxenter.de
Griffon Das Tutorial In navigate setzen wir zuerst den Buttontext des aktuell ausgewählten Menüpunkts in die Eigenschaft currentelement in unserem Modell, die an die Statusleiste gebunden ist. Dann wechseln wir die Selektion in der ButtonGroup und füllen den Inhaltsbereich mit der View des ausgewählten Menüpunkts. Zuletzt feuern wir ein Event Navigation, damit unsere Anwendung auf diese Selektion reagieren kann. Das Eventsystem Griffons Eventsystem ist zweigeteilt in Build-Events und Application-Events. Build- Events ermöglichen es, auf den Build-Prozess zu reagieren oder ihn gar zu beeinflussen. Jedes Griffon-Skript im Ordner scripts kann mit event("<eventname>", [<Parameter>] Nachrichten versenden. Im speziellen Griffon-Skript scripts/_ Events. können Listener registriert werden, indem man eine Closure mit dem Namen event<eventname> erstellt. Genauere Informationen sind im Griffon Guide, Kapitel 4 zu finden [6]. Application-Events sind Laufzeitevents. An einen zentralen Bus werden Events gesendet bzw. Listener registriert. An diesen Bus gelangen wir über die GriffonApplication, auf die wir von überall mittels der automatischen Variable app Zugriff haben: Mit app.event, app.eventasync und app.eventoutside, entsprechend der Logik des ThreadHandling aus dem vorigen Artikel, können Events versandt werden. Über app.addapplicationeventlistener werden Listener registriert. (Senior Consultant Java Technologies (m/w DSLs? J2SE & JEE? GUIs & RCP? Design & Architektur? Web, Mobile, Desktop? Um es dem Entwickler einfacher zu machen, sind automatisch alle Closures eines Controllers mit der folgenden Form als Listener registriert: on<eventname> = { args ->. So können z. B. in den Controllern der Inhaltsseiten Daten aktualisiert werden, sobald die View angezeigt wird: def onnavigation = { newmvc, oldmvc -> if(newmvc == 'overview' loaddata( Die Datei griffon-app/conf/events. fungiert als Platz für globale Event Handler, die schon vor dem Erstellen von MVCGroups funktionsbereit sind. Um eine Sicherheitsabfrage vor dem Schließen der Applikation zu implementieren, nutzen wir einen solchen globalen Event Handler und erstellen die Events. wie in Listing 8. Der Event Handler onbootstrapend wird aufgerufen, nachdem alle Plug-ins initialisiert wurden und bevor die erste MVCGroup erstellt wird. Zu diesem Zeitpunkt registrieren wir einen ShutdownHandler, der unsere Sicherheitsabfrage zeigt und ggf. das Beenden unterbricht. Im Griffon Guide, Kapitel 5 stehen nähere Details zu Application-Events und den Lifecycle-Events, die von Griffon gefeuert werden. Interessiert? Wir suchen Verstärkung für unser Java-Team! Listing 8: griffon-app/conf/events. import javax.swing.joptionpane onbootstrapend = { app -> app.addshutdownhandler([ canshutdown: { a -> return JOptionPane.showConfirmDialog( app.windowmanager.windows.find{it.focused, "Do you really want to exit?", "Exit", JOptionPane.YES_NO_OPTION == JOptionPane.YES_OPTION, onshutdown: { a -> ] as griffon.core.shutdownhandler Wir suchen Verstärkung für unser Java-Team! Weitere Informationen unter www.beone-group.com /stellenprofile Kontakt: evelyn.immegart@beone-group.com Tel. +49 711 656 93 100 Bewerbungsunterlagen bitte elektronisch an: recruitment@beone-group.com www.jaxenter.de javamagazin 9 2011 83
Das Tutorial Griffon Dienste Oft möchte man Funktionen zentralisieren, um sie wiederverwenden zu können. Das unterstützt Griffon mit dem Service-Konzept. Um den Zugriff auf unsere Datenbank zu zentralisieren, erstellen wir mittels griffon create-service database die Datei griffon-app/services/ babylonian/databaseservice. Ein Service ist eine reguläre Klasse, in der wir unsere Datenbankzugriffslogik implementieren können. Diese wird als Singleton behandelt, es wird von Griffon also nur eine Instanz automatisch instantiiert. Für Service-Klassen werden wie bei Controllern alle EventHandler Closures automatisch registriert. Um diese in einem Controller zu verwenden, müssen wir im Controller eine Variable mit dem Namen <ServiceName>Service erstellen. In unserem Fall also def databaseservice. Beim Erstellen des Controllers wird die Service-Instanz automatisch injiziert. Wenn von einem Service auf andere Services zugegriffen werden soll, benötigen wir Hilfe durch ein Inversionof-Control-Framework wie Spring, Guice oder Weld. Für diese drei Frameworks gibt es bereits Plug-ins, die wir z. B. mit griffon install-plugin spring installieren können. Nun sind auch Abhängigkeiten unter Services möglich. Der Quellcode und weitere Informationen Bis hierhin haben wir fast alle wichtigen Bereiche von Griffon behandelt. Der gesamte Quellcode des Babyloniers liegt bei und es sollte nun möglich sein, ihn zu verstehen. Er enthält Beispiele für Datenbankzugriffe, Tabellen, CRUD (Create/Read/Update/Delete usw. Der Quellcode verwendet weitere Plug-ins, die im Internet dokumentiert sind [3]. Als weitere Quellen für Griffon empfehle ich die Blogs von Andres Almiray [7], Nick Zhu [8] und mrhaki [9]. Zudem sind die Screencasts auf GriffonCast [10] sehenswert. Griffon beinhaltet Beispielanwendungen zur Veranschaulichung, dient aber auch als Hilfsmittel. Man findet sie unter $GRIFFON_HOME/samples auch unser Babylonier wird sich in Zukunft dort einreihen. Sehr zu empfehlen ist SwingPad (Abb. 3, eine der mitgelieferten Griffon-Beispielanwendungen, die das Erstellen von SwingBuilder-Code erheblich erleichtern. Während man auf der linken Seite Code schreibt, entsteht rechts daneben das Ergebnis. Zudem bringt Swing- Pad eine Menge an Beispielcode und Snippets mit. Fazit Griffon ist zwar zu großen Teilen in Groovy geschrieben und Groovy bietet als Sprache viele Vorteile, jedoch stellt die Verwendung von Groovy keinen Zwang dar. Es ist auch möglich, alle Artefakte in Java oder mittels Plug-ins auch in anderen Sprachen wie Clojure oder Scala zu schreiben. Griffon kann nicht zaubern, aber die Desktopentwicklung wird erheblich erleichtert. Die Kombination aus Convention over Configuration mit einer Vielzahl an Plug-ins liefert einen gut gefüllten Werkzeugkasten, ohne ein Korsett anzulegen. Abb. 2: Unsere fertige Applikation Overview Alexander Klein ist Senior Consultant bei der BeOne Stuttgart GmbH und seit 15 Jahren im Java-Umfeld als Entwickler, Architekt, Coach und Trainer hauptsächlich im Rich-Application-Umfeld tätig. Er ist bekennender Groovy-Jünger und Commiter bei Griffon. Links & Literatur Abb. 3: SwingPad: ein hilfreiches Tool in und für Griffon [1] Griffon-Webseite: http://griffon.codehaus.org [2] MiGLayout: http://www.miglayout.com [3] Griffon-Plug-ins: http://griffon.codehaus.org/plugins [4] Crystal Icons Set: http://everaldo.com/crystal [5] SwingBuilder: http://.codehaus.org/swing+builder [6] Griffon Guide: http://dist.codehaus.org/griffon/guide/index.html [7] Blog von Andres Almiray: http://www.jroller.com/aalmiray [8] Blog von Nick Zhu: http://nzhu.blogspot.com [9] Blog von mkhaki: http://mrhaki.blogspot.com [10] GriffonCast: http://griffoncast.com 84 javamagazin 9 2011 www.jaxenter.de