Grails: Portlets Teil 2

Im ersten Teil des Tutorials konnte man ja gut erkennen, dass Grails in einem Portlet lauffähig ist. Da das generierte WAR so an die 23 MB umfasst, sollte es alle relevanten Grails-Bibliotheken enthalten.

Beispiel

Nun ist es an der Zeit, eine kleine Anwendung zu erstellen. Dafür erstellen wir eine kleine Verwaltungssoftware für Fluglinien.

Plugins

calendar

Domainklassen

Zunächst sollen die folgenden drei Domainklassen verwaltet werden können: Flughafen, Flugzeuge und Flüge.

package de.ronnyfriedland

class Airport {

    String name
    String code

    static constraints = {
    }
}
package de.ronnyfriedland

class Plane {

    String type

    static constraints = {
    }
}
package de.ronnyfriedland

import java.util.Date;

class Flight {

    static belongsTo = [plane:de.ronnyfriedland.Plane,
                        start:de.ronnyfriedland.Airport,
                        destination:de.ronnyfriedland.Airport]

    String number
    Date date

    static constraints = {
      number(nullable: false, blank: false, maxSize: 250)
      date(nullable: false)
      plane(nullable: false)
      start(nullable: false)
      destination(nullable: false)
    }
}

Service

So richtig Sinn macht es zwar für dieses kleine Beispiel nicht - aber die so können wir überprüfen, ob Services richtig aus der Portletklasse heraus aufgerufen werden können.

package de.ronnyfriedland

class AirlineService {

  boolean transactional = true

  def listFlights() {
    return Flight.list()
  }

  def listAirports() {
    return Airport.list()
  }

  def listPlanes() {
    return Plane.list()
  }

}

Portletklasse

Für diesen Teil erstellen wir eine neues Portlet SecondPortlet, welches den VIEW-Mode und den HELP-Mode unterstützen soll.

Im VIEW-Mode können neue Flüge über ein Formular hinzugefügt werden. Der HELP-Mode enthält eine Auflistung aller Flughäfen, Flugzeuge und Flüge.

import java.text.SimpleDateFormat;
import javax.portlet.*

import de.ronnyfriedland.AirlineService
import de.ronnyfriedland.Airport
import de.ronnyfriedland.Flight
import de.ronnyfriedland.Plane

class SecondPortlet {

  def title = 'Flight Administration'
  def description = '''
Description about the portlet goes here.
'''
  def displayName = 'Display Name'
  def supports = ['text/html':['view', 'help']]

  // Liferay server specific configurations
  def liferay_display_category = 'MyCategory'

  def airlineService

  def actionView = {

    if(params.save) {
      Flight.withTransaction { status ->

        Flight flight = new Flight()

        if(params.date) {
          SimpleDateFormat format = new SimpleDateFormat("M/d/y", Locale.ENGLISH);
          Date date = format.parse(params.date_value);
          flight.date = date
        }

        if(params.number) {
          flight.number = params.number
        }

        if(params.plane) {
          def plane = Plane.findById(params.plane)
          flight.plane = plane
        }

        if(params.start) {
          def start = Airport.findById(params.start)
          flight.start = start
        }

        if(params.destination) {
          def destination = Airport.findById(params.destination)
          flight.destination = destination
        }

        if(!flight.validate()) {
          flash.error = "Angaben ungültig. Flug konnte nicht gespeichert werden: " + flight.errors
          return
        }

        flight.save(flush:true)
        flash.message = "Flug erfolgerich gespeichert."
        return
      }
    }
  }

  def renderView = {
    def airportList = airlineService.listAirports()
    def planeList = airlineService.listPlanes()

    ['airportList':airportList, 'planeList':planeList]
  }

  def actionHelp = {
    portletResponse.setPortletMode(PortletMode.VIEW)
  }

  def renderHelp = {
    def airportList = airlineService.listAirports()
    def planeList = airlineService.listPlanes()
    def flightList = airlineService.listFlights()

    ['airportList':airportList, 'planeList':planeList, 'flightList':flightList]
  }
}

Views

view.gsp

Dieser View soll das Formular enthalten, um neue Flüge speichern zu können. Jetzt kommt auch das calendar-Plugin zum Einsatz, um das Datum des Flugs über diesen JS-DatePicker auszuwählen.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>

<calendar:resources/>

 <g:if test="${flash.message}">
    <div class="message">
      ${flash.message}
      ${flash.message = null}
    </div>
 </g:if>
 <g:if test="${flash.error}">
    <div class="errors">
      <ul>
              <li>
                      ${flash.error}
                      ${flash.error = null}
              </li>
      </ul>
    </div>
 </g:if>

<div>
 <h1>Add new Flight</h1>
 <form action="${portletResponse.createActionURL()}">
      <p>
              <label for="number">Flight Number:</label><g:textField id="number" name="number" value="" />
      </p>
      <p>
              <label for="date">Date:</label><calendar:datePicker name="date" defaultValue="${new Date()}"/>
      </p>
      <p>
              <label for="start">Start Airport:</label>
              <g:select name="start"
                id="start"
        from="${airportList}"
        value="${id}"
        optionKey="id"
        optionValue="name" />
      </p>
      <p>
              <label for="destination">Destination Airport:</label>
              <g:select name="destination"
                id="destination"
        from="${airportList}"
        value="${name}"
        optionKey="id"
        optionValue="name" />

      </p>
      <p>
              <label for="plane">Plane:</label>
              <g:select name="plane"
                id="plane"
        from="${planeList}"
        value="${id}"
        optionKey="id"
        optionValue="type" />
      </p>

  <input type="submit" name="save" value="Save"/>
 </form>
</div>

image0

help.gsp

Dieser View soll eigentlich nur alle Daten auflisten, die im System gespeichert sind.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<div>
 <form action="${portletResponse.createActionURL()}">

      <h1>Available Airports</h1>

      <table border="1">
              <tr>
                      <th>ID</th>
                      <th>Code</th>
                      <th>Name</th>
              </tr>
              <g:each in="${airportList}" var="airport" status="airportState">
                      <tr>
                              <td>${airport.id}</td>
                              <td>${airport.code}</td>
                              <td>${airport.name}</td>
                      </tr>
              </g:each>
      </table>

      <hr/>

      <h1>Available Planes</h1>

      <table border="1">
              <tr>
                      <th>ID</th>
                      <th>Type</th>
              </tr>
              <g:each in="${planeList}" var="plane" status="planeState">
                      <tr>
                              <td>${plane.id}</td>
                              <td>${plane.type}</td>
                      </tr>
              </g:each>
      </table>

      <hr/>

      <h1>Available Flights</h1>

      <table border="1">
              <tr>
                      <th>ID</th>
                      <th>Number</th>
                      <th>Date</th>
                      <th>Plane</th>
                      <th>Start</th>
                      <th>Destination</th>
              </tr>
              <g:each in="${flightList}" var="flight" status="flightState">
                      <tr>
                              <td>${flight.id}</td>
                              <td>${flight.number}</td>
                              <td>${flight.date}</td>
                              <td>${flight.plane.type}</td>
                              <td>${flight.start.name}</td>
                              <td>${flight.destination.name}</td>
                      </tr>
              </g:each>
      </table>

      <br/>

  <input type="submit" value="Back"/>
 </form>

</div>

image1

Beispieldaten

Das Beispiel hier sieht nur vor, Flüge hinzufügen zu können. Um das Beispiel etwas einfacher zu halten, habe ich mich entschlossen, Flugzeuge und Flughäfen bereits im Bootstrap zu erzeugen.

import de.ronnyfriedland.Airport;
import de.ronnyfriedland.Flight;
import de.ronnyfriedland.Plane;

import java.util.Date;

class BootStrap {

  def init = { servletContext ->
    Airport airport1 = new Airport()
    airport1.name = "Dresden"
    airport1.code = "DRS"
    airport1.save(flush:true)

    Airport airport2 = new Airport()
    airport2.name = "Rom"
    airport2.code = "CIA"
    airport2.save(flush:true)

    Airport airport3 = new Airport()
    airport3.name = "Dubai"
    airport3.code = "DXB"
    airport3.save(flush:true)

    Plane plane1 = new Plane()
    plane1.type = "Airbus A380"
    plane1.save(flush:true)

    Plane plane2 = new Plane()
    plane2.type = "Boing 737"
    plane2.save(flush:true)

    Flight flight = new Flight()
    flight.date = new Date()
    flight.number = "IA-123"
    flight.start = airport1
    flight.destination = airport2
    flight.plane = plane1
    flight.save(flush:true)

  }

  def destroy = {
  }
}

Probleme

Leider habe ich es nicht geschafft, ein Command Object für die Formulardaten zum laufen zu bekommen. Irgendwie war das Objekt immer null. Daher die etwas unschöne Lösung beim Speichern der Daten.

Beim Speichern erhielt ich die folgende Exception:
No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here
Abhilfe schaffte da, den gesammten Block in eine Transaktion zu packen:
...
Flight.withTransaction { status ->
 ...
}
...

Download

Das gesamte Beispiel gibt es hier als Download.