The purpose of this assignment is to learn how users of your website can upload files and later view them on your website. You will learn to modify a domain class and gsp views. In addition, you will learn the details of form input and image displaying. As a bonus, you will also learn about Geolocation and Leaflet.
You will continue working on the book store website. The book store client asks that images of the book cover should appear in the list of books on the books index page. The bookstore client also wants that his administrator should be able to upload images of the cover. Your client’s request implies that the Book domain class should have a “cover” property that either stores the image file or the URI (file path) to the image file. In this tutorial, we’ll store the image file in the database. Grails makes storing the image file in the database easy.
Consider the client’s demands, your modifications to the website are:
- The Book domain by adding a “cover” property
- The views/book/create.gsp and edit.gsp to upload the cover image file
- The Book controller by adding an action to stream the image in the database to the view
- The views/book/show by adding the cover image to the view
- Finally modify the Books controller and the views/books/index.gsp to add the cover image to the list
Step 1: Modify Book Domain
Read about file upload in the Grails documentation
http://grails.github.io/grails-doc/latest/guide/theWebLayer.html#uploadingFiles
Study in particular the section “File Uploads through Data Binding.”
Add the “cover” property to the Book domain:
byte[] cover
We also need to add to the constraints mapping. The primary role of constraints are for specifying constraints on the database, and how fields are validated. You can read about constraints and validation in the documentation:
http://docs.grails.org/latest/guide/single.html#validation
The Quick Reference has a list of constraints that you can use. In the Quick Reference click the “Constraints” link and a list of constraints will appear. The constraints we will use are:
- http://docs.grails.org/latest/ref/Constraints/nullable.html
- http://docs.grails.org/latest/ref/Constraints/blank.html
- http://docs.grails.org/latest/ref/Constraints/maxSize.html
Add to the Book domain class the constraints below:
static constraints = { title nullable: false, blank: false author nullable: false, blank: false publishYear nullable: false cover nullable: true, maxSize: 1024*1024*2 }
The cover maxSize must be specified or the database will not be created with an entry large enough to hold an image file. Also note that 2 megabytes might not be large enough for photos in your citizen science project. Notice that the string fields use the constraints “blank: false”, this prevents the user from enter an empty string.
We also need to configure the controller so that it will except files larger then 128000 byte. In application.yml add:
grails: controllers: upload: maxFileSize: 2097152 maxRequestSize: 2097152
Note that application.yml is a XML file and not a groovy file, so you can not have operations for entries. The entries must be numbers or strings, boolean etc.
I learn this configuration by reading blogs and stackoverflow:
- https://www.bookstack.cn/read/grails-v4.0/spilt.10.d41467982d8e0ffe.md
- http://beaumontmuni.blogspot.com/2016/09/grails-3.html
- https://stackoverflow.com/questions/29845943/grails3-file-upload-maxfilesize-limit
Note that maxFileSize specifies the size limit for any individual file while maxRequestSize specifies the size limit for the request, i.e. for the form.
Rerun Grails. Login as admin and navigate to the book index view. You will notice that “cover” has been added to the list of fields. You might also notice that the order is different. The constraints closure can also be used to specify the order of the fields in the view. There is another way to change the order which we’ll discuss in the next section.
If you navigate to the create view by clicking the “add new book” book, you’ll notice that the fields title and author are required. This is enforced by the “nullable: false” constraint. Actually, “nullable: false” is default setting, so we did not really have to specify them.
Step 2: Modify Book Views
1. Modify the Create View
If you try to create a book and upload a cover image, you’ll get an validation error:
Property cover is type-mismatched
This is because the form tag is wrong.
Open the views/book/create.gsp file in the editor. Near the bottom of the html is the g:form tag. This grails tag is for making a form on the page. Read about the form tag at w3schools
http://www.w3schools.com/html/html_forms.asp
You can read the general properties about g:form tags at
https://gsp.grails.org/latest/guide/index.html#formsAndFields
or in the Quick Reference at
- https://gsp.grails.org/latest/ref/Tags/form.html
- https://gsp.grails.org/latest/ref/Tags/submitButton.html
The g-submitButton tag made by the scaffolding uses the “value” attribute. The “value” attribute specifies the controller action that will be invoked when the form is submitted.
Our problem is that the form needs to be a “multipart form”
http://www.w3schools.com/tags/att_form_enctype.asp
There are two ways to modify the form tag so that it is mulitpart. One way is to specify the encoding of the submitted data by adding the attribute to the form tag
enctype="multipart/form-data"
so the form tag looks like
<g:form resource="${this.book}" method="POST" enctype="multipart/form-data">
The other way is to use g:uploadForm tag.
https://gsp.grails.org/latest/ref/Tags/uploadForm.html
So you can replace the g:form tag with the g:uploadForm tag
<g:uploadForm resource=${this.book" method="POST"> ... </g:uploadForm>
Use either technique. Refresh the page and you can upload a file. Please note it must be a small file because of the maxSize constraint.
Grails is using the Grails Fields Plugin for rendering form fields.
http://grails-fields-plugin.github.io/grails-fields/latest/guide/index.html
The plugin is convenient because by default it automatically creates the form based on the domain class members. This is convenient because as you add class members the form will automatically update. Look at the form code in views/book/create.gsp. Inside the fieldset tag is a f:all tag
<g:uploadForm resource="${this.book}" method="POST"> <fieldset class="form"> <f:all bean="book"/> </fieldset> <fieldset class="buttons"> <g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" /> </fieldset> </g:uploadForm>
You can read about the f:all tag at:
http://grails-fields-plugin.github.io/grails-fields/latest/ref/Tags/all.html
The f:all tag obscures the individual inputs and inhibits us from customizing and styling the form. For example, we want the cover file to be an image file and not any file, so we would like the file input dialog to show only image files. We can do this by adding an “accept” attribute to the file input tag. See:
https://www.w3schools.com/tags/att_input_accept.asp
So we need to replace the f:all tag with f:with tag
http://grails-fields-plugin.github.io/grails-fields/latest/ref/Tags/with.html
and then list the field separately. So replace the f:all tag in create.gsp with
<g:uploadForm resource="${this.book}" method="POST"> <fieldset class="form"> <!-- <f:all bean="book"/> --> <f:with bean="book"> <f:field property="title"/> <f:field property="author"/> <f:field property="publishYear"/> <f:field property="cover"/> </f:with> </fieldset> <fieldset class="buttons"> <g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" /> </fieldset> </g:uploadForm>
By the way, this is another way to order fields in a form. Perhaps a better way then specifying the order in the domain constraint closure because only the view should express the order not the domain class.
If you refresh the page. You’ll notice that nothing has changed, but now we have access to the individual inputs and can style them. Read about customized field rendering in the field guide:
We want to create a customized wrapper for the cover field. We can do this by creating a _wrapper.gsp in views/book/cover/. Make a cover directory in views/book/ directory. Then make a _wrapper.gsp file in the views/book/cover/ directory.
Add the code below to the file
<div class="fieldcontain ${hasErrors(bean: book, field: 'cover', 'error')} "> <label for="cover"> <g:message code="book.cover.label" default="Cover" /> </label> <input style="display:inline" type="file" name="cover" id="cover" accept="image/*" capture /> </div>
The code consist of three parts. The div class for expressing any errors from validation, but more to the point, a label tag for the “Cover” text in the input and an input tag.
The label tag uses grails messaging technique. It will look into the grails-app/i18n files for text of the file. If you open the grails-app/i18n/ directory, you will see a list of files named messages-<some country>.properties. Users can set language on their browsers and grails will look into the proper file for the text. The English text is in messages.properties. If you open that file in the editor you’ll see that it is just a list of values for keys into a map. In this case, grails with look for the “book.cover.label” entry. If it does not exist, it will use the default value, “Cover.” If you want to internationalize your app you would make entries for all text on your web in this file and in the other language files.
The label tag also defines the “for” attribute which specifies the element id of the input tag to associated with the label. In the example above this is “cover”, and the input tag specifies an id,
id="cover"
The id in the input-tag matches the “for” attribute in the label-tag.
Educate yourself on all the different form inputs by reading about input types at W3schools:
- http://www.w3schools.com/html/html_form_input_types.asp
- http://www.w3schools.com/html/html_form_attributes.asp
The input tag also sets the styling to “display:inline”. This is called inline styling, not because of the “inline” in “display:inline”, but because the styling is specified within the element tag using the style attribute. The value, “display:inline” forces the file upload box and button to appear in line with the label. Without this styling, the input tag is styled with display:block which will cause the file upload box and button to appear below the label.
Notice that the input tag also has the accept attribute specified
accept="image/*"
This will filter the file picker to show only image files. For an explanation see W3schools
http://www.w3schools.com/tags/att_input_accept.asp
Notice that the input-tag also has the capture attribute. This enables media capture devices. For image capture this is the camera. The user can choose between uploading a file or making a photo with the camera. See the API standard
https://www.w3.org/TR/html-media-capture/#the-capture-attribute
Now you can rerun grails and test the create book page. The styling is good and the image file uploads. Use dbconsole, to check that the file has really been saved to the database. You will need to add a static rule to the conf/application.groovy file.
[pattern: '/dbconsole/**', access: ['ROLE_ADMIN']],
Note that if you want the edit view (the view updating an entry), you will need to make these same changes to edit.gsp. Besides making the from multipart form, you will also need to change the method to “POST” from “PUT”.
2. Modify the Show View
We want the cover image to show in the book’s show view. Besides editing the view we will also have to use the book controller to retrieve the image from and render it to the view.
i. Add showCover action to BookController
Open the BookContoller in the editor, and add the method below to the controller:
def debugCover = true def showCover(){ println "In showCover" if(debugCover){ println " " println "In showCover" println "params.id: "+params.id } def book = Book.get(params.id) response.outputStream << book.cover response.outputStream.flush() }
Besides printing messages to the console, the action retrieves the book instance from the database using the get method:
http://docs.grails.org/latest/ref/Domain%20Classes/get.html
It uses the parameters in the URL to specify the id. The params variable is frequently used in grails coding. One use is to access the URL. When you select to show a book, the URL is for example:
http://localhost:8080/cs4760progassign/book/show/5
You already know that the first part, “//localhost:8080”, specifies the domain and the port number. The second part, “cs4760progassign”, is specifying the app. The third part, “book”, is specifying the controller, and the fourth part, “show”, is specifying the action. The fifth and final part, in this case “5” is specifying the id. So when the controller asks for params.id with this URL, it will be given 5. The params variable can give much more information. The Quick Reference
http://docs.grails.org/latest/ref/Controllers/params.html
does not give much information. But you can search for params in
http://docs.grails.org/latest/guide/theWebLayer.html
to see the many uses of params.
To display the cover image, we need to stream the database entry into the response. Read about the “response” object in the reference manual:
http://www.grails.org/doc/latest/ref/Servlet%20API/response.html
The example demonstrates the use of response.outputStream with the bit shift operator, “<<“.
ii. Modify the Show View
Open the views/book/show.gsp file in the editor. Near the bottom of the code you will find a f:display tag. As in the create view, we need to access the individual fields. So we replace the f:display tag with the code below
<!-- <f:display bean="book" /> --> <ol class="property-list book"> <f:with bean="book"> <f:display property="title" widegt-label="Title" /> <f:display property="author" label="Author"/> <f:display property="publishYear" label="Publication Year"/> <f:display property="cover"/> </f:with> </ol>
Now we can overwrite the cover like we did for the create and edit view. Unfortunately for display wrappers you have to overwrite all the fields. Make the views/book/show directory. Now we need to make sub-directories in the book/show/ directory. You cannot use IntelliJ IDEA to make the sub directories. The easiest way is to use file explorer to make the directories. IntelliJ will automatically notice that you have made the directories and add them to the project panel. Using the file explorer make sub-directories title/, author/, publishYear/, and cover/ in the views/book/show/ directory.
In the views/book/show/cover/ directory add a file named _displayWrapper.gsp using IntelliJ IDEA. Add the code below to the file
<g:if test="${book?.cover}"> <li class="fieldcontain"> <span id="cover-label" class="property-label"> <g:message code="book.cover.label" default="Cover: " /> </span> <img class="property-value" alt="Missing Cover" src="${createLink(controller:'book', action:'showCover', id:"${book.id}")}"> </li> </g:if>
The code first checks if that the cover exist in the book instance, useing a g:f tag surrounding the list item. Read about the g:if tag in the GSP reference manual
https://gsp.grails.org/latest/ref/Tags/if.html
The g:if tag checks that the book instance has the cover property. It then renders the label using the message file or in this case the default value. Finally it uses an img tag to locate the image in the view. You can read about img tags at:
http://www.w3schools.com/tags/tag_img.asp
The image is sourced using the createLink tag as a GSP method:
https://gsp.grails.org/latest/ref/Tags/createLink.html
To retrieve the image, it will ask for it from the BookController.showCover action giving it the book id as a parameter.
In the views/book/show/author/ directory add a _displayWrapper.gsp file, and then add the code below to the file
<g:if test="${book?.author}"> <li class="fieldcontain"> <span id="author-label" class="property-label"> <g:message code="book.author.label" default="Author" /> </span> <span class="property-value"> <g:link controller="author" action="show" id="${book?.author?.id}"> ${book?.author?.encodeAsHTML()} </g:link> </span> </li> </g:if>
The code should looks very familiar. The only difference is that now the author field is rendered with encodeAsHTML().
http://docs.grails.org/latest/guide/security.html#codecs
Also notice that there is a link around the author name to the author show view.
In the views/book/show/publishYear/ directory add a _displayWrapper.gsp file, and then add the code below to the file
<g:if test="${book?.publishYear}"> <li class="fieldcontain"> <span id="publishYear-label" class="property-label"> <g:message code="book.publishYear.label" default="Publication Year" /> </span> <span class="property-value" aria-labelledby="title-label"> <g:fieldValue bean="${book}" field="publishYear"/> </span> </li> </g:if>
This should look very familiar. Now, we get the field value from the bean, so we do not need to encode it.
Finaly in the views/book/show/title/ directory add a _displayWrapper.gsp file. Add the code below to the file
<g:if test="${book?.title}"> <li class="fieldcontain"> <span id="title-label" class="property-label"> <g:message code="book.title.label" default="Title" /> </span> <span class="property-value" aria-labelledby="title-label"> <g:fieldValue bean="${book}" field="title"/> </span> </li> </g:if>
And this is all very familiar .
Now re-run grails and create a new book. You should see the cover image displayed in the show view.
Now you maybe asking, “How did you know to make all this code?” I used two tools. The first was my experience with prior versions of Grails that did not use field plugin to scaffold the views. The second was this blog:
“http://blog.anorakgirl.co.uk/2016/01/what-the-f-is-ftable/“
Step 5: Add Map to the Home Page
This step is a diversion using the Geolocation API and Leaflet JavaScript library. Our goal is detect the user’s current location and display it on a map in the home page.
1. Use geolocation
Read about using the geolocation object at
https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation
You can learn more about the Gelocation API at
http://www.w3.org/TR/geolocation-API/
On the home page, views/index.gsp, we need to add a div for the latitude and longitude to display. In addition, we’ll add a div for where to display the map. At the bottom of views/index.gsp add the code below just above the terminating body-tag.
<!-- Simple get location --> <p style="margin-top:20px"><button onclick="geoFindMe()" >Show my location</button></p> <div id="latlong-out"></div> <div id="mapid" style="height: 400px; width: 400px;"></div> <asset:javascript src="geoloc.js"/>
The button will call the geoFindMe function. We will add this function in a JavaScript file. We’ll display the latitude and longitude in the div with id equal to “latlong-out” and the map will display in the div with id equal to “map-canvas”. Note that the map-canvas must have a width and height, or otherwise it will not show.
We write the geoFindMe function in a JavaScript file called geoloc.js. We need to include the JavaScript file, geoloc.js, to the view/index.gsp in order to access the navigator. Because the JavaScript in geoloc.js will only be used on this page of the website, we should load it only on this page. We will use the asset-pipeline to include the file at the end of body.
Now we need to make the geoloc.js file. In grails-app/assets/javascripts/ add a file and name it geoloc.js. When the editor displays the blank page copy the code below into the file
function geoFindMe() { var outputLatLong = document.getElementById("latlong-out"); if (!navigator.geolocation){ outputLatLong.innerHTML = "<p>;Geolocation is not supported by your browser</p>"; return; } function success(position) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; outputLatLong.innerHTML = '<p>Latitude is ' + latitude + '° <br>Longitude is ' + longitude + '°</p>'; }; function error() { outputLatLong.innerHTML = "Unable to retrieve your location"; }; outputLatLong.innerHTML = "<p>Locating…</p>"; navigator.geolocation.getCurrentPosition(success, error); }
The geoloc.js code defines the geoFindMe function. The function first locates the html elements for writing the latitude and longitude. It is given the name outputLatLong in the script. Then the script checks if it can access the navigator.geolocation. If geolocation is not available, the script writes to outputLatLong informing the user that “GeoLocation is not supported by your browser.” This is a standard JavaScript design pattern. The design pattern is called “feature testing”. Before using an advance feature, i.e. a HTML5 API, the code should first check that it can access the feature. If not then offer an alternative or inform the user that the request cannot be made. As a developer your can check which browser support a feature by using “Can I use?” website.
For example, try
https://caniuse.com/#feat=geolocation
Nearly all browsers have been supporting GeoLocation for sometime. Note that at the bottom of the page are useful information. For example look at the “Notes” which informs that “only works on secure (https) servers”. Most browser permit using GeoLocation on localhost without being secure to enable development.
The device location is retrieved by
navigator.geolocation.getCurrentPosition(success, error);
The getCurrentPostion takes two callback functions, success and error. The function success is called if the browser can get the location or otherwise the error function is called. The script needs to define these callbacks.
The success has a single argument, position, which is a Position JavaScript object.
https://developer.mozilla.org/en-US/docs/Web/API/Position
Position has coords property which the script can use to access the latitude and longitude. Finally the latitude and longitude are displayed on the page.
Launch your website and test. Click the button. A modal should appear telling you that the website is accessing your location and if you want to allow or block. Be sure to allow or GeoLocation will not be accessed and the location of the device will not be available.
2. Use Leaflet Map API
Leaflet is an opensource JavaScript library for mobile-friendly interactive maps. It is a preferred library for “slippy maps” using Open Street Maps (OSM).
Read about adding a Leaflet at
https://leafletjs.com/examples/quick-start/
You can learn more about Leaflet API at
https://leafletjs.com/reference
i. Display the Map
First we need to include the the Leaflet styles and JavaScript on the page. In views/index.gps, and add line below to the bottom of the head of the view just above the terminating head-tag, </head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/> <!-- Make sure you put this AFTER Leaflet's CSS --> <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
Now we need to modify the script in geoloc.js to use the Leaflet library. In the geoloc.js file, add the code below to the bottom of the success function.
// Add map var mymap = L.map('mapid').setView([latitude, longitude], 13); var tileURL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' var attribution = '<a href="https://www.openstreetmap.org/copyright">;OpenStreetMap</a> contributors' var maxZoom = 19 L.tileLayer(tileURL, { attribution: attribution, maxZoom: maxZoom }).addTo(mymap); // end add map
The map is created by L.map() generator function. Note that “L” is a global variable for the Leaflet library. This is a standard technique in JavaScript for accessing libraries. Note that you should not make a variable “L” in your script or you’ll not be able to access the Leaflet library. L.map() requires the id of the div for the map location in the web page. Chaining is used to set the view of the map which is specified by an array of latitude and longitude, and the zoom level. The higher the number the more zoom-in and the smaller the extent.
We need “tile server” to provide the maps for browser to display in the div. The URL for the tile sever is specified.
var tileURL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
The variables in the URL {s}, {z}, {x} and {y} specifies the server and the requested title. Title servers provide map sections which are typically 256×256 pixels. The tiles are specified by their center x,y and the zoom level, z. To display the map, Leaflet makes several call to the title sever.
It is consider good practice to include an attribution, the map creator. The attribution is the HTML that is displayed at the bottom of the map by Leaflet.
The last property to specify before making the tile layer is the maximum zoom level. Each title server has it own specific maximum zoom level. You may want to specify a lower zoom level.
The tile layer is created with L.tileLayer() generator function. The generation function requires template for title server URL and an optional TileLayer options.
Refresh the index page. Notice that you can drag the map. This is why the map is called “slippy”.
ii. Add Marker to the Map
A nice feature of Leaflet is adding objects to the map. For example, an marker icon marking the current location. Add the code below just after adding the title layer to the map.
// Add marker var marker = L.marker([latitude, longitude], { draggable: false, // set true to enable dragging the marker autoPan: false // set true so the maps pans with the marker }).addTo(mymap) // end add marker
The marker is created with an array of latitude and longitude, and the marker options object. Chaining is used to add the marker to the map. You can learn of the marker options draggable and autoPan at:
https://leafletjs.com/reference#marker
Refresh the page and click the “Show my location” button. Notice how you can drag the map but not the marker.Try changing the draggalble markerOptions property and confirm your guess what draggable means.
iii. Add Marker Event Listener and Popup
Another nice feature of Leaflet is that you can generate events (usually initiated by the user) and respond to the events via a listener. Add the code below to geoFindMe just after adding the marker to the map.
// Add listener for marker moved function onMarkerMove(e){ marker.bindPopup("Marker at "+e.latlng.toString()).openPopup(); } marker.on('move', onMarkerMove); // end add listener
The function onMarkerMove is our listener for the move event. It gets Event object. The marker move event is augmented with the latlng property that we can use to retrieve the latitude and longitude of the marker on the map. The onMarkerMove listener binds a popup to the marker with a content specifying the latitude and longitude. Chaining is used to open the popup. The listener function is attached to the marker move event with the “on” method specifying the event to listener to and the listener.
Refresh the page. Try moving the marker. Be sure that the marker is created with draggable set to true.
iv. Change the Tile Sever
The tutorial uses OpenStreetMap.
https://wiki.openstreetmap.org/wiki/About_OpenStreetMap
It is one of my favorite maps. It may not be the most appropriate map for your app’s purposes. A Leaflet plugin, leaflet-providers, simplifies the process for adding a title server.
https://github.com/leaflet-extras/leaflet-providers
Leaflet-providers has a very nice preview page at:
http://leaflet-extras.github.io/leaflet-providers/preview/
In the preview, you can select the title server in the overlay on the right of the displayed map. In the top center overlay, you can see the L.tileLayer to make to use the tile server in Leaflet.
Select a tile server a different title server and edit geoFindMe function to display the map on Book Store home page. Make a screen shot of the home page with the new title server.
Step 6: Modify books/index.gsp
You are on your own for this step. The goal is to display the book cover in the list of books on the books public index page. Show the cover above or below each list item for the book in the list.
Some hints:
- You’ll probably want to add the book.id to the bkAuthor map in the BooksController.index action.
- Try styling the img-tag using twitter bootstrap. Try the img-thumbnail class.
http://www.w3schools.com/bootstrap/bootstrap_ref_css_images.asp
Step 7: Screen Screen Shots and Submit
When you are done, make a screen shoot of the book index page. Also make a screen shot of the Book Store home page with a new title server. Submit the screen shots to “Programming Assignment 4 – Uploading and Displaying Images”.