Monday, May 19, 2014
Monday, October 21, 2013
Ganelon tutorial, part 2: widgets and actions.
If you want to see the final result of an entire tutorial, you can view the tutorial app running live at http://ganelon-tutorial.tomeklipski.com or you can check out/star/fork the source code at http://github.com/tlipski/ganelon-tutorial.
For more information regarding Ganelon, you can visit the project website: http://ganelon.tomeklipski.com.
Connecting to MongoDB
Using mongodb is Clojure project is very easy and there's nothing Ganelon-specific about it.
First we will add dependency for congomongo library ([congomongo "0.4.1"]
) to project.clj
:
:dependencies [[ganelon "0.9.0"] [congomongo "0.4.1"] [crypto-random "1.1.0"]]
There's also another new library referenced - crypto-random
. It is a useful tool when it comes to generating secure random identifiers - something that will come in very handy when creating new meetups or invitations.
Having the MongoDB library in place, we can add connection setup to src/ganelon/tutorial.clj:
(ns ganelon.tutorial (:gen-class) (:require [ganelon.tutorial.pages.routes] [ring.middleware.stacktrace] [ring.middleware.reload] [ganelon.web.middleware :as middleware] [ganelon.web.app :as webapp] [noir.session :as sess] [somnium.congomongo :as db])) (defn get-mongo-url [] (or (get (System/getenv) "MONGOHQ_URL") (System/getProperty "MONGOHQ_URL") "mongodb://localhost/meetups")) (defn initialize[] (db/set-connection! (db/make-connection (get-mongo-url))))
The get-mongo-url
function returns mongo db url either from environment or from default development setup. In real-life scenario, the connection options will be much more complex, including username&password, failover options, etc.
It is also noteworthy, that we don't have to manage MongoDB connections ourselves - the underlying libraries are taking care of that for us.
The initialize
function is referenced in lein-ring plugin configuration and will be invoked upon ring application initialization.
Accessing the data with MongoDB
To separate our application from underlying persistence layer, we will create seperate service namespaces for basic data integration. Fortunately, most of the work is done by congomongo library and we can use native Clojure data structures in all layers.
Data operations are defined in three namespaces:
- src/ganelon/tutorial/services/meetup.clj - meetup entity management.
- src/ganelon/tutorial/services/meetup_time.clj - possible times for each meetup.
- src/ganelon/tutorial/services/invitation.clj - meetup invitations.
All of functions in these namespace are quite simple and mostly cover congomongo (somnium.congomongo
is imported as db
) references, for example:
(defn retrieve-list [meetup-id] (db/fetch :meetup-times :where {:meetup-id meetup-id} :sort {:date 1 :time 1})) (defn add-time! [meetup-id date time] (when (empty? (db/fetch :meetup-times :where {:meetup-id meetup-id :date date :time time})) (db/insert! :meetup-times {:meetup-id meetup-id :date date :time time :create-time (java.util.Date.) :accepted []})))
Defining widgets
Finally, having all the other components in place, we are able to create some Ganelon-specific widgets and actions!
New meetup widget
The new meetup widget consists of two parts, listed below. Entire definition is contained in ganelon.tutorial.widgets.meetup-add namespace.
First of all, we have widget definition. As you can see below, it is a standard Clojure function, and it is invoked explicitly from Clojure code - ganelon.tutorial.pages.routes/meetup-layout
to be exact.
(defn new-meetup-widget [] (widgets/with-div [:h1 "New meetup"] [:p "Please provide meetup details:"] (widgets/action-form "meetup-create" {} {:class "form well"} [:div.control-group [:label.control-label {:for "inputTitle"} "Title"] [:div.controls [:input#inputTitle {:placeholder "Title for a meetup" :type "text" :name "title" :required "1"}]]] [:div.control-group [:label.control-label {:for "inputPlace"} "Place"] [:div.controls [:input#inputPlace {:placeholder "Place for a meetup" :type "text" :name "place" :required "1"}]]] [:div.control-group [:div.controls [:button.btn.btn-primary.btn-large {:type "submit"} "Create"]]])))
We use Hiccup to generate HTML form, styled by Bootstrap. One significant difference is that we don't use [:form]
HTML tag, but rather ganelon.web.widgets/action-form
function. This function allows us to reference a Ganelon action to be invoked upon form submission - client side.
Action meetup-create
, creating new meetup is defined with a help of a ganelon.web.actions/defjsonaction
macro.
(actions/defjsonaction "meetup-create" [title place] (let [id (meetup/create! title place)] [(ui-operations/open-page (str "/meetup/edit/" id))]))
The action itself is pretty straightforward:
- First we create new meetup in MongoDB using form params - the destructuring syntax underneath is Compojure's, since we are using its handler infrastructure.
- Then, we return one operation to be performed - to redirect the browser to meetup's edit page, containing its random and unique id.
In the next part of the tutorial, this action will change and will not reload entire page - and we will use custom JavaScript actions to achieve that.
Meetup details widget (and sub-widgets)
Meetup details widget is more complicated than the previous one, since it has to support not only edition of meetup place and title, but also:
- Allow the user to manage possible times for a meetup with response statuses, which is implemented in
ganelon.tutorial.widgets.meetup-times
namespace. - Manage meetup invitations - in
ganelon.tutorial.widgets.meetup-invitations
namespace.
Please observe, that the main meetup details widget is referencing sub-widgets as functions: meetup-edit-form-widget
, meetup-times/meetup-times-widget
and meetup-invitations/meetup-invitations-widget
. This way, we can decompose our web application into a set of widgets with clearly defined dependencies.
(defn meetup-details-widget [meetup-id] (widgets/with-div (let [meetup (meetup/retrieve meetup-id)] [:div [:div {:style "padding-top: 20px;"} (meetup-edit-form-widget meetup)] (meetup-times/meetup-times-widget meetup-id) (meetup-invitations/meetup-invitations-widget meetup-id)])))
As we have more possibilities here than just meetup creation, we can make the form more dynamic. For example, we will capture :onchange
JavaScript events for meetup title and place to update the fields in MongoDB without save button. Even more, we will display a confirmation label next to a saved field.
In meetup edition widget definition, we will add standard HTML attributes, referencing our Ganelon action (meetup-title-update) - but client-side! As Ganelon has information about defined actions, there is an interface available as a standard Compojure route (by default under /ganelon/actions.js), publishing all these functions to be referenced in JavaScript client-side:
(defn meetup-edit-form-widget [meetup] (widgets/with-div [:h1 "Meetup details"] (let [url (str (web-helpers/current-request-host-part) "/meetup/edit/" (:_id meetup))] [:p "Meetup admin url: " [:a {:href url } url]]) [:form.form-horizontal.well [:div.control-group [:label.control-label {:for "inputTitle"} "Title"] [:div.controls [:input#inputTitle.input-xlarge {:placeholder "Title for a meetup" :type "text" :value (:title meetup) :onkeypress "$('#update-title-loader > *').fadeOut();" :onchange (str "GanelonAction.meetup_title_update('" (:_id meetup) "', this.value);") :name "title" :required "1"}] [:span#update-title-loader]]] [:div.control-group [:label.control-label {:for "inputPlace"} "Place"] [:div.controls [:input#inputPlace.input-xlarge {:placeholder "Place for a meetup" :type "text" :value (:place meetup) :onkeypress "$('#update-place-loader > *').fadeOut();" :onchange (str "GanelonAction.meetup_place_update('" (:_id meetup) "', this.value);") :name "place" :required "1"}] [:span#update-place-loader]]]]))
Once more, we are using standard Bootstrap 2 classes here to render a form.
Actions update meetup details in MongoDB, but also update form elements with jQuery's fade effect, provided by ganelon.web.ui-operations/fade
function - for example:
(actions/defjsonaction "meetup-title-update" [id title] (meetup/update! id :title title) [(ui-operations/fade (str ".meetup-title-" id) (hiccup.util/escape-html title)) (ui-operations/fade "#update-title-loader" (hiccup.core/html [:span {:onmouseover "$(this).fadeOut();"} " " [:i.icon-check] " Saved"]))])
You are welcome to browse the operations available in ganelon.web.ui-operations
namespace - but please note, that it is also very easy to define your own, more fitting for you apps specific needs!
Meetup times widget allows us to add new invitations and to list and manage existing ones - including invitation confirmation status for each time and invitation. The whole implementation is available in ganelon.tutorial.widgets.meetup-times
namespace.
(defn toggle-meetup-times-button [t inv editable-invitation-ids] (let [accepted? (some #{(:_id inv)} (:accepted t))] (widgets/with-div [:div {:style (str "padding: 8px; " (if accepted? "background-color: #5bb75b" "background-color: #9da0a4"))} (if (or (not editable-invitation-ids) (some #{(:_id inv)} editable-invitation-ids)) (widgets/action-loader-link "meetup-times-toggle-invitation" {:invitation-id (:_id inv) :id (:_id t) :value (not accepted?)} {} (if accepted? [:i.icon-thumbs-up ] [:i.icon-thumbs-down ])) " ")]))) (defn meetup-times-list-widget [meetup-id editable-invitation-ids] (widgets/with-widget "meetup-times-list-widget" (let [times (meetup-time/retrieve-list meetup-id)] (if (not-empty times) (let [invitations (invitation/retrieve-list meetup-id)] [:table.table.table-striped.table-hover {:style "width: initial"} [:thead [:tr (when-not editable-invitation-ids [:th ]) [:th "Date"] [:th "Time"] (for [inv invitations] [:th [:small (hiccup.util/escape-html (:name inv))]])]] (for [t times] [:tr (when-not editable-invitation-ids [:td (widgets/action-link "meetup-remove-time" {:id (:_id t)} {} [:i.icon-remove ])]) [:td (:date t)] [:td (:time t)] (for [inv invitations] [:td {:style "text-align: center; padding:0px; border-left: 1px solid #ccc"} (toggle-meetup-times-button t inv editable-invitation-ids)])])]) [:div.alert [:i "No meetup times defined yet!"]]))))
Please observe, that we are using widgets in loop here - as ganelon.web.widgets/with-div
macro binds widget each time to a unique and random value, we can tons of instances of the same widget side by side.
We can also define a function which will return Ganelon UI operation refreshing times list - to be invoked externally (e.g. from invitations list widget):
(defn refresh-meetup-times-list-widget-operations [meetup-id] (ui-operations/fade "#meetup-times-list-widget" (meetup-times-list-widget meetup-id nil)))
This operation references widget by id attribute, allowing us to invoke it without widget-id context set.
The meetup-times-toggle-invitation
action takes advantage of Compojure's route params support, allowing us to access them by name directly:
(actions/defwidgetaction "meetup-times-toggle-invitation" [id invitation-id value] (if (boolean (Boolean. value)) (meetup-time/accept-time! id invitation-id) (meetup-time/reject-time! id invitation-id)) (toggle-meetup-times-button (meetup-time/retrieve id) (invitation/retrieve invitation-id) [invitation-id]))
meetup-add-time
action itself provides simple validation mechanism:
(actions/defjsonaction "meetup-add-time" [id date time] (if-let [new-mu (meetup-time/add-time! id date time)] ;success [(ui-operations/make-empty "#meetup-add-time-message") (ui-operations/fade "#meetup-times-list-widget" (meetup-times-list-widget id nil))] ;error - such time already exists (ui-operations/fade "#meetup-add-time-message" (hiccup.core/html [:div.alert [:button.close {:type "button" :data-dismiss "alert"} "×"] [:p "Such date & time combination already exists!" ]]))))
ganelon.web.ui-operations/make-empty
operation simply removes all contents from designated dom element.
Meetup invitations list widget works on similar principle as meetup times widget listed above:
(defn meetup-invitations-widget [meetup-id] (widgets/with-widget "meetup-invitations-widget" [:h2 "Meeting invitations"] (widgets/action-form "invitation-create" {:meetup-id meetup-id} {:class "form-horizontal well"} [:span "Recipient's name:"] " " [:input {:type "text" :name "name" :required "1"}] " " [:button.btn.btn-primary "Create new invitation"] ) (let [invitations (invitation/retrieve-list meetup-id)] (if (empty? invitations) [:div.alert "No invitations created yet. Please use the form above to add some!"] (for [inv invitations] [:p [:b (hiccup.util/escape-html (:name inv))] [:br] "Link: " [:a {:href (str (web-helpers/current-request-host-part) "/i/" (:_id inv))} (str (web-helpers/current-request-host-part) "/i/" (:_id inv))] (widgets/action-link "invitation-cancel" {:meetup-id meetup-id :id (:_id inv)} {:class "pull-right"} [:i.icon-remove] " Cancel")]))))) (actions/defwidgetaction "invitation-create" [name meetup-id] (invitation/create! meetup-id name) (actions/put-operation! (meetup-times/refresh-meetup-times-list-widget-operations meetup-id)) (meetup-invitations-widget meetup-id)) (actions/defwidgetaction "invitation-cancel" [meetup-id id] (invitation/delete! id) (actions/put-operation! (meetup-times/refresh-meetup-times-list-widget-operations meetup-id)) (meetup-invitations-widget meetup-id))
One thing worth noticing is that we put operation refreshing meetup times widget upon invitation creation or deletion. Instead of accessing widget functions for meetup times directly, we call a wrapper function which encapsulates all the necessary logic - e.g. DIV identification and returns ready to use Ganelon UI operation.
Invitation widget
Invitation widget is used by meetup attendees and allows them only to confirm or reject their presence at the meetup at given time. Whole implementation is provided withing ganelon.tutorial.widgets.invitation-details
namespace.
(defn invitation-details-widget [id] (widgets/with-div (if-let [inv (invitation/retrieve id)] (let [meetup (meetup/retrieve (:meetup-id inv))] [:div [:div [:h2 (hiccup.util/escape-html (:title meetup))] [:p "Located at: " [:b (hiccup.util/escape-html (:place meetup))]] [:p "Sent to: " [:b (hiccup.util/escape-html (:name inv))]]] [:h2 "Confirm your presence"] [:p "Please mark times & dates that are best for you:"] (meetup-times/meetup-times-list-widget (:meetup-id inv) [id])]) [:div.alert.alert-error [:h2 "Invitation not found"] [:p "Invitation with a supplied id has not been found. Please make sure that the link is correct."]])))
Please note, that we are re-using meetup-times/meetup-times-list-widget
to provide meetup times and to allow the attendee to confirm or negate his/her presence at the meetup. We do so by providing additional parameter to function invocation, limiting displayed invitations only to the currently used.
Summary
In this blog post we have learned, that it is very easy to build dynamic, AJAX-oriented web application entirely in Clojure, server side. Even better, such approach allows us to re-use existing components in different scenarios. We don't have to stick with our application being single-page and we are totally 100% SEO friendly in such approach, as widgets are used both in standard HTML page rendering as in updates to the page itself.
In the next post I will show how to add middleware for security handling and how to define custom Ganelon UI operations in JavaScript.
Tuesday, August 20, 2013
Ganelon tutorial, part 1: basic setup, routes & templates
If you want to see the final result of an entire tutorial, you can view the tutorial app running live at http://ganelon-tutorial.tomeklipski.com or you can check out/star/fork the source code at http://github.com/tlipski/ganelon-tutorial.
For more information regarding Ganelon, you can visit the project website: http://ganelon.tomeklipski.com.
Creating Ganelon project
lein new
to start a new project or set up the project.clj
file and src/
, script/
and resources/
directories manually.In either way, it is important that the project.clj file contains a dependency on ganelon 0.9.0 library. What is worth mentioning, is that ganelon comes with dependencies on lib-noir, Compojure & hiccup, so in simple cases, it is enough to just add this one dependency when it comes to Clojure web apps.
Ganelon 0.9.0 is deployed to clojars, so it will get fetched alongside other Clojure libraries.
Our project.clj
at this moment looks like this:
(defproject ganelon-tutorial "0.9-SNAPSHOT" :description "Ganelon tutorial" :url "http://ganelon.tomeklipski.com" :dependencies [[ganelon "0.9.0"] [org.clojure/clojure "1.5.1"]] :license {:name "Eclipse Public License - v 1.0" :url "http://www.eclipse.org/legal/epl-v10.html" :distribution :repo :comments "same as Clojure"} :plugins [[lein-ring "0.8.6"]] :ring {:handler ganelon.tutorial/handler :init ganelon.tutorial/initialize})
As we can see, there is only one dependency other than Clojure 1.5.1 - it is for ganelon 0.9.0. There is also a lein-ring configuration with a pretty standard settings - an initialize function, which at this moment does nothing but will establish a connection pool for MongoDB and a handler reference.
Handler definition
Our handler will be using Ganelon's simple mechanism for establishing routes - and we will mix app-handler
from Ganelon with standard Ring middleware.
You can add your own handlers and middleware here and it will just work. If you don't want to or cannot use Compojure routes, you can just skip this step altogether and use raw Ring handlers from ganelon.web.actions package.
(def handler (-> (ganelon.web.app/app-handler (ganelon.web.app/javascript-actions-route)) middleware/wrap-x-forwarded-for (ring.middleware.stacktrace/wrap-stacktrace) (ring.middleware.reload/wrap-reload {:dirs ["src/ganelon/tutorial/pages"]})))
View the entire ganelon/tutorial.clj file.
Please also note, that we are using a convienent ring.middleware.reload/wrap-reload
middleware, which will reload all the changes to Clojure files in specified directories - src/ganelon/tutorial/pages
in this case.
Defining the Hiccup templates
The simplest way (but sometimes not the most effective) to achieve common layout and HTML code for pages with Hiccup is to define them as functions, accepting page content as parameters:
(defn layout [& content] (hiccup/html5 [:head [:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}] [:title "Ganelon tutorial - Meetups"] ;real life site should use CDN/minify to serve static resources (hiccup/include-css "/ganelon/css/bootstrap.css") (hiccup/include-css "/ganelon/css/bootstrap-responsive.css") ] [:body.default-body [:div#navbar (navbar)] [:div.container {:style "padding-top: 70px"} content] [:footer {:style "opacity:0.9; text-align: center; padding: 30px 0; margin-top: 70px; border-top: 1px solid #E5E5E5; color: #f6f6f6; background-color: #161616;"} [:div.container [:p "The Ganelon framework has been designed, created and is maintained by " [:a {:href "http://twitter.com/tomeklipski"} "@tomeklipski"] "."] [:p "The code is available under " [:a {:href "http://opensource.org/licenses/eclipse-1.0.php"} "Eclipse Public License 1.0"] "."] [:p [:a {:href "http://github.com/tlipski/ganelon-tutorial"} "View the sources on GitHub."]] [:p "This interactive tutorial runs on " [:a {:href "http://cloudbees.com"} "CloudBees"] " and " [:a {:href "http://mongohq.com"} "MongoHQ"] "."] ]]]))
View the entire ganelon/tutorial/pages/common.clj file.
As you can see, we are using standard Bootstrap2 CSS/classes - nothing fancy.
We have also defined a helper layout with common UI components and put it routes.clj file:
(defn meetup-layout [& contents] (common/layout [:div.row-fluid [:div.span3 [:div {:style "border: 1px dashed #363636"} "TODO - new meetup widget here"] [:div {:style "border: 1px dashed #363636"} "TODO - meetup list widget here"]] [:div.span1 ] [:div.span8 [:div#contents contents]]]))
Defining routes
With templates already defined, we can set up our routes. They will contain placeholders for widgets, as these will be added in the next steps of the tutorial:
(dyna-routes/defpage "/" [] (meetup-layout [:div.hero-unit [:h1 "Welcome"] [:p "Welcome to the interactive tutorial for " [:a {:href "http://ganelon.tomeklipski.com"} "Ganelon micro-framework."]] [:p "This sample application used to manage meetups provides links to display source of every widget and action used. In addition to that, each widget has a dashed border to mark its boundary."]])) (dyna-routes/defpage "/meetup/edit/:id" [id] (meetup-layout [:div {:style "border: 1px dashed #363636"} "TODO - meetup details widget here"])) (dyna-routes/defpage "/i/:id" [id] (meetup-layout [:div {:style "border: 1px dashed #363636"} "TODO - invitation details widget here"]))
The ganelon.web.dyna-routes/defpage
macro simply creates compojure.core/ANY
route and adds it to a list maintained in the ganelon.web.dyna-routes
namespace. The routes can be grouped for easier management and middleware can be defined in a similar way. More information is available on the Routing page of Ganelon project website.
Running the project
After that, we have two general ways to run our project and there is nothing specific to Ganelon about them:
The simplest is to use lein-ring plugin and run lein ring server
:
$ lein ring server 2013-08-19 21:39:41.474:INFO:oejs.Server:jetty-7.6.1.v20120215 2013-08-19 21:39:41.502:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000 Started server on port 3000
We can also use lein ring uberjar
to obtain an executable jar file.
If we are using some IDE not compliant with Leiningen - for example IntelliJ IDEA, we can create a simple script to run our jetty-ring adapter:
(ns run (:require [ganelon.tutorial] [ring.adapter.jetty :as jetty])) (defonce SERVER (atom nil)) (defn start-demo [port] (jetty/run-jetty ganelon.tutorial/handler {:port port :join? false})) (ganelon.tutorial/initialize) (let [port (Integer. (get (System/getenv) "PORT" "3000"))] (swap! SERVER (fn [s] (when s (.stop s)) (start-demo port))))
This scripts just optionally stops and then starts jetty adapter for our handler - but can be loaded for example into IntelliJ IDEA's REPL.
After applying either method, we can just navigate to http://localhost:3000/ to see the running application.
What's next?
In the next part of the tutorial, I will show how to build widgets, actions and use them to propagate data back and forth between a web app and a MongoDB instance.
Monday, August 19, 2013
Ganelon tutorial, intro
What is Ganelon?
How does Ganelon work?
Ganelon tutorial
- Setup a new Ganelon project, configure basic routes and layout templates (using Hiccup) (PART 1).
- Build reusable, dynamic elements of UI (widgets) and use MongoDB (from mongohq) as a persistence layer (PART 2).
- Add middleware for security handling and define custom operations (PART 3).
- Finally, I will show how can the tutorial app display its source code (PART 4).
Thursday, April 18, 2013
Ganelon 0.9.0 released
In addition to that, an interactive tutorial app (meetings management with MongoDB) has been launched at http://ganelon-tutorial.tomeklipski.com. Source codes for it can be found in GitHub repository: https://github.com/tlipski/ganelon-tutorial.
In the following weeks, this tutorial app will be used as a base for a tutorial blog series.
Using Ganelon
To use Ganelon in your Compojure/Ring/Clojure web application, simply add the following dependency to yourproject.clj
file:[ganelon "0.9.0"]For more information on using Ganelon, please visit http://ganelon.tomeklipski.com.
Production use
Most important features
As this is a first major release of Ganelon, instead of changes introduced, I will simply highlight most important features:- AJAX support for client-side operations, including basic functions and almost whole jQuery | Manipulation library.
- Ability to add custom client-side operations.
- Additional libraries for Bootstrap Modal JavaScript and jquery gritter plugin (Growl-style notifications).
- Support for definition of AJAX actions as Compojure routes.
- Basic functions rendering client-side code for HTML/JavaScript controls invoking AJAX actions.
- Support for distributed configuration of Compojure routes - somewhat akin to Noir's
defpage
macro.
Thursday, April 11, 2013
Running and debugging Clojure code with Intellij IDEA
These features make Clojure development with IDEA a real pleasure.
Installation of La Clojure plugin
Importing leiningen project
With the La Clojure plugin ready and active, we can import a leiningen project into IntelliJ IDEA. All we have to do is to generate appropriate Maven pom.xml file:
lein pom
Then, we can import the maven module using IntelliJ IDEA.
Step 1: select Import Project from welcome screen:
Starting the REPL
Interacting with REPL
- Run selected text in REPL
- Execute last S-Expression in REPL
- Run top S-Expression in REPL
Debugging with REPL
Step 1: we have to create a Remote Debugger profile using Run / Edit Configurations... from a menu.
To add a Remote Debugger, we have to click on the plus '+' sign and select 'Remote' configuration type:
We can adjust the settings or leave them as default. Most importantly, we have to copy command line arguments for running remote JVM, for example:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Tuesday, March 19, 2013
Introducing Ganelon - micro-framework supporting AJAX in Clojure/Ring web apps
Ganelon is fully Ring compatible and based on Compojure and lib-noir. Being a AJAX focused, it is not a direct replacement for Noir. It can rather ease the pain of handling dynamic page requests in any Ring-based web application.
The execution model is heavily influenced by Weblocks or Vaadin, but without session-statefulness and out-of-the-box rich user interface:
- Certain parts of the page can be scoped as Widgets (for example using id attribute in HTML or ganelon.web.widgets.with-div utility macro) and referenced by id in Actions, and updated by Operations. Widgets can reference Actions to be invoked.
- Actions are invoked as XHR requests and return a set of Operations to be performed client-side. Actions are in fact simple Ring handlers.
- Operations provide abstraction layer over client-side JavaScript execution - e.g. using Bootstrap Modal or just updating part of DOM tree (a Widget or any other) or open certain URL in a browser window - or just anything else that has a simple integration layer provided.
The source codes are on GitHub: http://github.com/tlipski/
Sample code (a shoutbox) is listed below:
;A Widget, returning HTML code:
(defn box-widget []
(widgets/with-div
[:p "Call count (since restart): " [:b @COUNTER] ". Last 4 entries:"]
(for [entry @ENTRIES]
[:div.hibox [:b (:time entry)] ": "
(hiccup.util/escape-html (:msg entry))])
(widgets/action-form "say-hi" {} {:class "form-inline"}
[:input {:name "msg" :placeholder "Say hi!" :type "text"
:maxlength "20" :size "20"}] " "
[:button {:class "btn btn-primary"} "Send!"])))
;An Action, performing side effects and returning part of the page to be updated
(actions/defwidgetaction "say-hi" [msg]
(swap! COUNTER inc)
(swap! ENTRIES #(util/smart-subvec (flatten [(mkmsg msg) %]) 0 4))
(box-widget))
JavaScript code uses jQuery and optionally Bootstrap, but the implementation is really trivial and therefore easy to replace using your favorite JS library.
Monday, March 18, 2013
Porting Activiti Explorer to Liferay Portal
Proof of concept fork with Tasks and Admin portlets is available in
https://github.com/tlipski/Activiti/tree/master/modules/activiti-portlets.
Porting Activiti Explorer to Liferay Portal
Please note, that embedding Activiti in portal is not the same as using it to manage portal assets - for example, as a replacement for Liferay's Kaleo Workflow. The matter discussed here is more focused on using all of Activiti features, including user interface.
Extracting and re-using entire components requires much more work in a classical MVC approach and even may not always be possible. Also, portlet support in Vaadin makes a whole task much more pleasant.
Necessary steps
- Portlet descriptors are necessary
- GUI should be divided into separate portlets
- Spring-Vaadin integration is different due to a portal-specific request life-cycle
- Navigation can be provided by portal - including friendly URLs, which makes for a more standard user experience.
- Users and Groups should be managed by Liferay Portal
- Authentication data should be taken from a portlet container
- Mail notifications could possibly use Liferay Mail API
- Distribution and management of Vaadin widgetset and version - we should use Vaadin Control Panel portlet and Liferay to manage Vaadin dependencies. This requires special handling, since Activiti Explorer 5.13 utilizes dCharts Vaadin Add-on.
- Vaadin theme used in portlets - which is also customized by Activiti, but should be coherent with portal's look & feel.
Current state of work
- Activiti Tasks portlet, which works in a user's context as if the user would log in to Activiti Explorer app. The portlet provides full functionality, including operations as starting a new task, fetching it from a task list, displaying task's events, assignment/ownership transfers, file attachments and so on.
- Activiti Admin portlet in Liferay Control Panel - most tabs seem to work fine - except Users and Groups, which should be disabled as we are using Liferay Portal to do that.
Implementation details:
- IdentityProvider implementation utilizing Liferay API (need to work on user photos though!)
- Spring-Vaadin bridging for portal (see my previous post).
- Custom portlet applications and main Window class. The interesting detail here is that with Vaadin portlets, you should not set width of components to 100%, as it will result in 0px height. Generally speaking, the height in Vaadin portlet components should be set to undefined - null, or a preset value in pixels.
Next steps?
- More portlets! Reporting and Processes tabs need their respective portlets, but also "start process" portlet might be worth considering since we can manage UI contents in a more flexible way.
- More testing! If anyone is interested in testing Activiti portlets in Liferay, let me know and I will provide prebuilt applications. So far, I've used Liferay 6.1 GA2 CE and H2 in-memory database for Activiti.
- Friendly urls - especially for tasks resolved by id, and for interportlet communication - e.g. when starting a process.
Sunday, February 24, 2013
Integrating Spring, Vaadin & Liferay
In such approach, there are two major differences between Vaadin with Spring in a standard web application and portlets-based one. This post is based on Vaadin 6, but the rules itself apply to Vaadin 7 as well.
The setup
The solution requires that we will overwrite two methods defined incom.vaadin.terminal.gwt.server.AbstractApplicationPortlet2
.To achieve that, we have to create our own subclass:
package com.tomeklipski.blog.sample.vaadin; import com.vaadin.Application; import com.vaadin.terminal.gwt.server.ApplicationPortlet2; import javax.portlet.*; import java.io.IOException; /** * Created with IntelliJ IDEA. * * @author tomek@lipski.net.pl * Date: 2/23/13 4:06 PM */ public class MyApplicationPortlet extends ApplicationPortlet2 { /* overwritten methods here */ }The newly created class has to be used in web.xml:
<servlet> <servlet-name>VaadinPortletServlet</servlet-name> <servlet-class>com.liferay.portal.kernel.servlet.PortletServlet</servlet-class> <init-param> <param-name>portlet-class</param-name> <param-value>com.tomeklipski.blog.sample.vaadin.MyApplicationPortlet</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet>And in portlet.xml as well:
<portlet> <portlet-name>MyPortlet</portlet-name> <display-name>My Sample Portlet</display-name> <portlet-class>com.tomeklipski.blog.sample.vaadin.MyApplicationPortlet</portlet-class> <init-param> <name>application</name> <value>com.tomeklipski.sample.vaadin.SomeApplication</value> </init-param> <init-param> <name>widgetset</name> <value>com.vaadin.portal.gwt.PortalDefaultWidgetSet</value> </init-param> <supports> <mime-type>text/html</mime-type> <portlet-mode>view</portlet-mode> </supports> <portlet-info> <title>My Sample Portlet</title> <short-title>Sample</short-title> </portlet-info> </portlet>
Accessing Spring context in portlet
First of all - portlets are configured using listeners as well. Liferay adds its listeners to web.xml during deploy, so listeners in web.xml might look like this:<listener> <listener-class> com.liferay.portal.kernel.servlet.PluginContextListener </listener-class> </listener> <listener> <listener-class> com.liferay.portal.kernel.servlet.SerializableSessionAttributeListener </listener-class> </listener> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <listener> <listener-class> org.springframework.web.context.request.RequestContextListener </listener-class> </listener> <listener> <listener-class> com.liferay.portal.kernel.servlet.PortletContextListener </listener-class> </listener>
As you can see, they are all mixed up and we cannot rely that during invocation of method
javax.portlet.Portlet.init(PortletConfig config)
the spring context is available.If we want to refer to our application as a Spring bean, we have to overwrite method
com.vaadin.terminal.gwt.server.AbstractApplicationPortlet.getNewApplication(PortletRequest request)
, and attempt to retrieve Spring context here:@Override protected Application getNewApplication(PortletRequest request) { PortletContext portletContext = getPortletContext(); ApplicationContext webApplicationContext = PortletApplicationContextUtils.getWebApplicationContext(portletContext); Application app = (Application) webApplicationContext.getBean("app"); return app; }The fetching of portlet and web application contexts can of course be optimized.
Accessing "session" scope
The "session" scope in Spring is a convient way to store objects related to a session, for example:<bean name="app" class="com.tomeklipski.sample.vaadin.SomeApplication" scope="session"> <property name="dataManager" ref="dataManager" /> <property name="i18nManager" ref="i18nManager" /> </bean>In standard web application, we can enable it using HttpRequestListener in web.xml:
<listener> <listener-class> org.springframework.web.context.request.RequestContextListener </listener-class> </listener>But, in JSR286 portlet, the listener is not invoked as it should and portlet specification does not provide the ability to add portlet request listeners as above.
Luckily, we can overwrite the method
com.vaadin.terminal.gwt.server.AbstractApplicationPortlet.handleRequest(PortletRequest request, PortletResponse response)
with a code, that will set appropriate ThreadContext values:@Override protected void handleRequest(PortletRequest request, PortletResponse response) throws PortletException, IOException { LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContextHolder.setLocaleContext( new SimpleLocaleContext(request.getLocale()), false); // Expose current RequestAttributes to current thread. RequestAttributes previousRequestAttributes = RequestContextHolder .getRequestAttributes(); PortletRequestAttributes requestAttributes = null; if (previousRequestAttributes == null || previousRequestAttributes.getClass() .equals(PortletRequestAttributes.class)) { requestAttributes = new PortletRequestAttributes(request); RequestContextHolder.setRequestAttributes(requestAttributes, false); } try { super.handleRequest(request, response); } finally { LocaleContextHolder.setLocaleContext(previousLocaleContext, false); if (requestAttributes != null) { RequestContextHolder.setRequestAttributes( previousRequestAttributes, false); requestAttributes.requestCompleted(); } } }
Summary
It is fairly easy to use Spring in Vaadin portlets - but there are some differences between standard Vaadin-Spring application approach and portletized one. We can easily overcome them by overwriting two methods incom.vaadin.terminal.gwt.server.AbstractApplicationPortlet
.
Sunday, May 6, 2012
#Clojure , #MongoDB and removing a set of items
When using congomongo library to access MongoDB in Clojure, it is sometimes required to remove a collection of items. As the example on GitHub page shows only how to remove one item, I have pasted the code here.
Setup test data
To setup test data, we would use 100 items in :points collection.
(use 'somnium.congomongo) (mass-insert! :points (for [x (range 0 10) y (range 0 10)] {:x x :y y})) (count (fetch :points)) ;=> 100
Please note, that I am not writing any code specific to establishing a connection here. For more information on using congomongo library, please visit congomongo github site.
The obvious way
The most obvious way to do so, would be to pass a list objects to destroy! function. Sadly, it won't work:
user=> (destroy! :points (fetch :points)) ;=> ClassCastException clojure.lang.LazySeq cannot be ;=> cast to com.mongodb.DBObject ;=> somnium.congomongo/destroy! (congomongo.clj:419)
Using for loop
We can also invoke destroy!
a hundred times:
(count (fetch :points)) ;=> 100 (for [p (fetch :points)] (destroy! :points p)) (count (fetch :points)) ;=> 0
It will work, but it is not optimal.
Using where clause
The best way to remove a collection of items in MongoDB using congomongo, it would be to use where clause for a destroy!
function:
(count (fetch :points)) ;=> 100 (destroy! :points {:x {:$gt 4}}) (count (fetch :points)) ;=> 50
It is also possible to refer MongoDB items by their ids in where clause:
(count (fetch :points)) ;=> 100 (destroy! :points {:_id {:$in (map :_id (fetch :points))}}) (count (fetch :points)) ;=> 0
Monday, April 23, 2012
Hacking Activiti BPM engine: how to use custom MyBatis queries
The issue
One big change that Activiti has introduced over jBPMv4 was to forego Hibernate ORM and use MyBatis. MyBatis is simpler and cleaner to use, but - it does not provide simple interface to perform queries that are not defined in annotations or XML files.
And even though Activiti provides great query interface, so you can do things like this:
List<Task> tasks = getProcessEngine().getTaskService().createTaskQuery() .taskName(taskName) .executionId(taskExecutionId) .taskAssignee(user.getLogin()) .listPage(0, 1);
with relative ease, there are still some limitations to the API provided by Activiti Service classes (e.g. you cannot add IN clause for task assignees).
Possible solutions
One thing that you can do, is to just skip Activiti interface, and access database directly. Surely it will work, but such approach has many downsides - you will be fetching information about Activiti processes in two different ways, you will have to map data to Activiti objects yourself, and so on.
What we have managed to do for Aperte Workflow <-> Activiti integration (in version 2.0, where API is a bit more demanding), is indeed a hack. It is not as elegant as constructing query for jBPM, but still is quite simple and manageable. It propably can be done with Spring in a similiar fashion, or with SelectBuilder - but the principle is similiar.
The hack
1. Enhance Activiti's MyBatis mapping file - by adding our own version in a different package.
The enhanced file looks almost like the original - with one exception:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="lazyLoadingEnabled" value="false" /> </settings> <mappers> <mapper resource="org/activiti/db/mapping/entity/Attachment.xml" /> <mapper resource="org/activiti/db/mapping/entity/Comment.xml" /> <mapper resource="org/activiti/db/mapping/entity/Deployment.xml" /> <mapper resource="org/activiti/db/mapping/entity/Execution.xml" /> <mapper resource="org/activiti/db/mapping/entity/Group.xml" /> <mapper resource="org/activiti/db/mapping/entity/HistoricActivityInstance.xml" /> <mapper resource="org/activiti/db/mapping/entity/HistoricDetail.xml" /> <mapper resource="org/activiti/db/mapping/entity/HistoricProcessInstance.xml" /> <mapper resource="org/activiti/db/mapping/entity/HistoricTaskInstance.xml" /> <mapper resource="org/activiti/db/mapping/entity/IdentityInfo.xml" /> <mapper resource="org/activiti/db/mapping/entity/IdentityLink.xml" /> <mapper resource="org/activiti/db/mapping/entity/Job.xml" /> <mapper resource="org/activiti/db/mapping/entity/Membership.xml" /> <mapper resource="org/activiti/db/mapping/entity/ProcessDefinition.xml" /> <mapper resource="org/activiti/db/mapping/entity/Property.xml" /> <mapper resource="org/activiti/db/mapping/entity/Resource.xml" /> <mapper resource="org/activiti/db/mapping/entity/TableData.xml" /> <mapper resource="org/activiti/db/mapping/entity/Task.xml" /> <mapper resource="org/activiti/db/mapping/entity/User.xml" /> <mapper resource="org/activiti/db/mapping/entity/VariableInstance.xml" /> <mapper resource="org/aperteworkflow/ext/activiti/mybatis/Task-enhanced.xml" /> </mappers> </configuration>
And the addition is:
<mapper resource="org/aperteworkflow/ext/activiti/mybatis/Task-enhanced.xml" />
2. Create enhanced mapper resource
This mapper resource add additional query conditions to the initial task query. The beginning of it is the same as original, but at the end we have added support for in clauses on several fields:
<!-- new conditions --> <if test="owners != null && owners.size() > 0"> and T.OWNER_ IN <foreach item="owner" index="index" collection="owners" open="(" separator="," close=")"> #{owner} </foreach> </if> <if test="notOwners != null && notOwners.size() > 0"> and T.OWNER_ NOT IN <foreach item="owner" index="index" collection="notOwners" open="(" separator="," close=")"> #{owner} </foreach> </if> <if test="groups != null && groups.size() > 0"> and T.ASSIGNEE_ is null and I.TYPE_ = 'candidate' and I.GROUP_ID_ IN <foreach item="group" index="index" collection="groups" open="(" separator="," close=")"> #{group} </foreach> </if> <if test="taskNames != null && taskNames.size() > 0"> and T.NAME_ IN <foreach item="name" index="index" collection="taskNames" open="(" separator="," close=")"> #{name} </foreach> </if> </foreach> </where> </sql>
The entire mapper file can be downloaded here from Aperte Workflow's github repository - core/activiti-context/src/main/resources/org/aperteworkflow/ext/activiti/mybatis/Task-enhanced.xml.
3. Introduce new configuration to MyBatis
As MyBatis runs inside of Activiti internals, we have to access and alter Activiti configuration mechanisms. To do that, we simply override one of the methods with our copy (which points to a new mapping file):
public class CustomStandaloneProcessEngineConfiguration extends StandaloneProcessEngineConfiguration { @Override protected void initSqlSessionFactory() { if (sqlSessionFactory == null) { InputStream inputStream = null; try { inputStream = ReflectUtil.getResourceAsStream( "org/aperteworkflow/ext/activiti/mybatis/mappings-enhanced.xml"); // update the jdbc parameters to the configured ones... Environment environment = new Environment("default", transactionFactory, dataSource); Reader reader = new InputStreamReader(inputStream); XMLConfigBuilder parser = new XMLConfigBuilder(reader); Configuration configuration = parser.getConfiguration(); configuration.setEnvironment(environment); configuration.getTypeHandlerRegistry().register(VariableType.class, JdbcType.VARCHAR, new IbatisVariableTypeHandler()); configuration = parser.parse(); sqlSessionFactory = new DefaultSqlSessionFactory(configuration); } catch (Exception e) { throw new ActivitiException( "Error while building ibatis SqlSessionFactory: " + e.getMessage(), e); } finally { IoUtil.closeSilently(inputStream); } } } }
You can of course do lots of amazing customizations here. And use this new class for Activiti BPM engine initialization:
CustomStandaloneProcessEngineConfiguration customStandaloneProcessEngineConfiguration = new CustomStandaloneProcessEngineConfiguration(); customStandaloneProcessEngineConfiguration .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE) .setDataSource(getDataSourceWrapper(sess)) .setHistory(ProcessEngineConfiguration.HISTORY_FULL) .setTransactionsExternallyManaged(true);
4. Enhance query object
Activiti uses wonderful, clean and simple query objects, that provide structure to MyBatis query parameters. To introduce new parameters, all we have to do, is to enhance TaskQueryImpl class with our own:
package org.aperteworkflow.ext.activiti.mybatis; import org.activiti.engine.impl.TaskQueryImpl; import java.util.HashSet; import java.util.Set; /** * @author tlipski@bluesoft.net.pl */ public class TaskQueryImplEnhanced extends TaskQueryImpl { private Set<String> creators = new HashSet<String>(); private Set<String> owners = new HashSet<String>(); private Set<String> groups = new HashSet<String>(); private Set<String> notOwners = new HashSet<String>(); private Set<String> taskNames = new HashSet<String>(); public Set<String> getGroups() { return groups; } public Set<String> getNotOwners() { return notOwners; } public Set<String> getOwners() { return owners; } public Set<String> getCreators() { return owners; } public Set<String> getTaskNames() { return taskNames; } public TaskQueryImplEnhanced addTaskName(String name) { taskNames.add(name); return this; } public TaskQueryImplEnhanced addOwner(String login) { owners.add(login); return this; } public TaskQueryImplEnhanced addGroup(String name) { groups.add(name); return this; } public TaskQueryImplEnhanced addNotOwner(String login) { notOwners.add(login); return this; } public TaskQueryImplEnhanced addCreator(String login) { creators.add(login); return this; } }
5. Invoke new query
And finally, we can invoke our new query with multiple assignees and other custom where clauses:
final TaskQueryImplEnhanced q = new TaskQueryImplEnhanced(); for (UserData u : filter.getOwners()) { q.addOwner(u.getLogin()); } for (UserData u : filter.getCreators()) { q.addCreator(u.getLogin()); } for (UserData u : filter.getNotOwners()) { q.addNotOwner(u.getLogin()); } for (String qn : filter.getQueues()) { q.addGroup(qn); } ActivitiContextFactoryImpl.CustomStandaloneProcessEngineConfiguration processEngineConfiguration = getProcessEngineConfiguration(); CommandExecutor commandExecutorTxRequired = processEngineConfiguration .getCommandExecutorTxRequired(); List<Task> tasks = commandExecutorTxRequired.execute( new Command<List<Task>>() { @Override public List<Task> execute(CommandContext commandContext) { return commandContext.getDbSqlSession() .selectList("selectTaskByQueryCriteria_Enhanced", q); } });
Please note, that we are also exposing CommandExecutor object instance to access DbSqlSession.
Summary
The technique presented here would be unnecessary or much simpler if Activiti would provide external means to configure MyBatis. Maybe that is a thing, that will be available in the future versions of Activiti.
Still, even at this moment, it is fairly easy to enhance/alter Activiti's behaviour. I haven't seen any final classes (a common sight in Hibernate - e.g. org.hibernate.impl.SessionImpl) and the internals of Activiti are quite simple to understand.