diff --git a/bbb-lti/.asscache b/bbb-lti/.asscache
deleted file mode 100644
index be6deceb5e9795140859e536f78de8fe794cf1ef..0000000000000000000000000000000000000000
Binary files a/bbb-lti/.asscache and /dev/null differ
diff --git a/bbb-lti/.gitignore b/bbb-lti/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b1bf7c47d2acad6cf5cb5bab1e95b7914a7e881f
--- /dev/null
+++ b/bbb-lti/.gitignore
@@ -0,0 +1 @@
+.asscache
\ No newline at end of file
diff --git a/bbb-lti/application.properties b/bbb-lti/application.properties
index 68fe1dc6a3086a523d0d680d0a2aa94498698311..a00806dd9e3917149f9cd48666f6b864737f9235 100644
--- a/bbb-lti/application.properties
+++ b/bbb-lti/application.properties
@@ -2,4 +2,5 @@
 #Fri Aug 19 19:12:11 UTC 2016
 app.grails.version=2.5.2
 app.name=lti
-app.version=0.3
+app.servlet.version=3.0
+app.version=0.4
diff --git a/bbb-lti/grails-app/conf/BuildConfig.groovy b/bbb-lti/grails-app/conf/BuildConfig.groovy
index 0bf647fec3f6cb2567a104182888511ffe2e6c43..4a84c2785ab79613b455443f1f6ee09cdabe83cd 100644
--- a/bbb-lti/grails-app/conf/BuildConfig.groovy
+++ b/bbb-lti/grails-app/conf/BuildConfig.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 grails.servlet.version = "3.0" // Change depending on target container compliance (2.5 or 3.0)
 grails.project.class.dir = "target/classes"
@@ -65,6 +65,7 @@ grails.project.dependency.resolution = {
     }
 
     dependencies {
+        compile 'org.json:json:20171018'
         // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes e.g.
         // runtime 'mysql:mysql-connector-java:5.1.29'
         // runtime 'org.postgresql:postgresql:9.3-1101-jdbc41'
diff --git a/bbb-lti/grails-app/conf/Config.groovy b/bbb-lti/grails-app/conf/Config.groovy
index 40ca78588a826c528ffc3ddcb9782db2082841fb..966ea2329ee7c256de724ec2e5bb1e5d0a0aba8f 100644
--- a/bbb-lti/grails-app/conf/Config.groovy
+++ b/bbb-lti/grails-app/conf/Config.groovy
@@ -1,4 +1,4 @@
-/* 
+/*
     BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
     Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
@@ -14,7 +14,7 @@
 
     You should have received a copy of the GNU Lesser General Public License along
     with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*/    
+*/
 
 // locations to search for config files that get merged into the main config;
 // config files can be ConfigSlurper scripts, Java properties files, or classes
@@ -103,6 +103,9 @@ grails.hibernate.pass.readonly = false
 // configure passing read-only to OSIV session by default, requires "singleSession = false" OSIV mode
 grails.hibernate.osiv.readonly = false
 
+// Enable hot reloading for production environments
+grails.gsp.enable.reload=true
+
 environments {
     development {
         grails.logging.jul.usebridge = true
diff --git a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
index 600218fedfb3fb3544939bf3798492c7fb3d19d5..b868a2b564c5cab3425ed9f20a5345b9b8095bd2 100644
--- a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
+++ b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy
@@ -48,125 +48,103 @@ class ToolController {
 
     def index = {
         log.debug CONTROLLER_NAME + "#index"
-        if( ltiService.consumerMap == null) ltiService.initConsumerMap()
-
+        if (ltiService.consumerMap == null) {
+            ltiService.initConsumerMap()
+        }
         setLocalization(params)
-
         params.put(REQUEST_METHOD, request.getMethod().toUpperCase())
         ltiService.logParameters(params)
-
-        if( request.post ){
-            def scheme = request.isSecure()? "https": "http"
-            def endPoint = scheme + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
-            log.info "endPoint: " + endPoint
-            Map<String, String> result = new HashMap<String, String>()
-            ArrayList<String> missingParams = new ArrayList<String>()
-
-            if (hasAllRequiredParams(params, missingParams)) {
-                def sanitizedParams = sanitizePrametersForBaseString(params)
-                def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
-                if ( !ltiService.hasRestrictedAccess() || consumer != null) {
-                    if (ltiService.hasRestrictedAccess() ) {
-                        log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
-                    }
-
-                    if (!ltiService.hasRestrictedAccess() || checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
-                        if (!ltiService.hasRestrictedAccess() ) {
-                            log.debug  "Access not restricted, valid signature is not required."
-                        } else {
-                            log.debug  "The message has a valid signature."
-                        }
-
-                        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
-                        if( !"extended".equals(mode) ) {
-                            log.debug  "LTI service running in simple mode."
-                            result = doJoinMeeting(params)
-                        } else {
-                            log.debug  "LTI service running in extended mode."
-                            if ( !Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault() ) {
-                                log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
-                                result = doJoinMeeting(params)
-                            }
-                        }
-
-                    } else {
-                        log.debug  "The message has NOT a valid signature."
-                        result.put("resultMessageKey", "InvalidSignature")
-                        result.put("resultMessage", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
-                    }
-
-                } else {
-                    result.put("resultMessageKey", "ConsumerNotFound")
-                    result.put("resultMessage", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
-                }
-
-            } else {
-                String missingStr = ""
-                for(String str:missingParams) {
-                    missingStr += str + ", ";
-                }
-                result.put("resultMessageKey", "MissingRequiredParameter")
-                result.put("resultMessage", "Missing parameters [$missingStr]")
+        // On get requests render the common cartridge.
+        if (request.get) {
+            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            return
+        }
+        // On post request proceed with the launch.
+        def endPoint = ltiService.getScheme(request) + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null ? "." + params.get("format") : "")
+        log.info "endPoint: " + endPoint
+        ArrayList<String> missingParams = new ArrayList<String>()
+
+        if (!hasAllRequiredParams(params, missingParams)) {
+            String missingStr = ""
+            for (String str:missingParams) {
+                missingStr += str + ", ";
             }
+            return renderError("MissingRequiredParameter", "Missing parameters [$missingStr]")
+        }
 
-            if( result.containsKey("resultMessageKey") ) {
-                log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-                render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
-
-            } else {
-                session["params"] = params
-                render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
+        def sanitizedParams = sanitizePrametersForBaseString(params)
+        def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
+        if (ltiService.hasRestrictedAccess()) {
+            if (consumer == null) {
+                return renderError("ConsumerNotFound", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
             }
+            log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
+        }
+        def validSignature = checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))
+        if (ltiService.hasRestrictedAccess()) {
+            if (!validSignature) {
+                log.debug  "The message has NOT a valid signature."
+                return renderError("InvalidSignature", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
+            }
+            log.debug  "The message has a valid signature."
         } else {
-            render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
+            log.debug  "Access not restricted, valid signature is not required."
+        }
+        def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
+        if (!"extended".equals(mode)) {
+            log.debug  "LTI service running in simple mode."
+            def result = doJoinMeeting(params)
+            return
         }
+        log.debug  "LTI service running in extended mode."
+        if (!Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) && !ltiService.allRecordedByDefault()) {
+            log.debug  "Parameter custom_record was not sent; immediately redirecting to BBB session!"
+            def result = doJoinMeeting(params)
+            return
+        }
+        session["params"] = params
+        render(view: "index", model: ['params': params, 'recordingList': getSanitizedRecordings(params), 'ismoderator': bigbluebuttonService.isModerator(params)])
     }
 
     def join = {
         if( ltiService.consumerMap == null) ltiService.initConsumerMap()
         log.debug CONTROLLER_NAME + "#join"
-        Map<String, String> result
-
+        def result
         def sessionParams = session["params"]
-
         if( sessionParams != null ) {
             log.debug "params: " + params
             log.debug "sessionParams: " + sessionParams
             result = doJoinMeeting(sessionParams)
         } else {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if (result != null && result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         }
     }
 
     def publish = {
         log.debug CONTROLLER_NAME + "#publish"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the publish command
+            // Execute the publish command
             result = bigbluebuttonService.doPublishRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -175,25 +153,22 @@ class ToolController {
     def delete = {
         log.debug CONTROLLER_NAME + "#delete"
         Map<String, String> result
-
         def sessionParams = session["params"]
-
         if( sessionParams == null ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidSession")
-            result.put("resultMessage", "Invalid session. User can not execute this action.")
+            result.put("messageKey", "InvalidSession")
+            result.put("message", "Invalid session. User can not execute this action.")
         } else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
             result = new HashMap<String, String>()
-            result.put("resultMessageKey", "NotAllowed")
-            result.put("resultMessage", "User not allowed to execute this action.")
+            result.put("messageKey", "NotAllowed")
+            result.put("message", "User not allowed to execute this action.")
         } else {
-            //Execute the delete command
+            // Execute the delete command.
             result = bigbluebuttonService.doDeleteRecordings(params)
         }
-
-        if( result.containsKey("resultMessageKey")) {
-            log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
-            render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
+        if( result.containsKey("messageKey")) {
+            log.debug "Error [messageKey:'" + result.get("messageKey") + "', message:'" + result.get("message") + "']"
+            render(view: "error", model: ['messageKey': result.get("messageKey"), 'message': result.get("message")])
         } else {
             render(view: "index", model: ['params': sessionParams, 'recordingList': getSanitizedRecordings(sessionParams), 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
         }
@@ -203,48 +178,39 @@ class ToolController {
         String locale = params.get(Parameter.LAUNCH_LOCALE)
         locale = (locale == null || locale.equals("")?"en":locale)
         String[] localeCodes = locale.split("_")
-        //Localize the default welcome message
-        if( localeCodes.length > 1 )
+        // Localize the default welcome message
+        session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        if (localeCodes.length > 1) {
             session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0], localeCodes[1])
-        else
-            session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
+        }
     }
 
     private Object doJoinMeeting(Map<String, String> params) {
-        Map<String, String> result = new HashMap<String, String>()
-
         setLocalization(params)
         String welcome = message(code: "bigbluebutton.welcome.header", args: ["\"{0}\"", "\"{1}\""]) + "<br>"
-
         // Check for [custom_]welcome parameter being passed from the LTI
-        if ( params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null ) {
+        if (params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null) {
             welcome = params.get(Parameter.CUSTOM_WELCOME) + "<br>"
             log.debug "Overriding default welcome message with: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault() ) {
+        if (params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.record") + "</b><br>"
             log.debug "Adding record warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
-        if ( params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0 ) {
+        if (params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0) {
             welcome += "<br><b>" + message(code: "bigbluebutton.welcome.duration", args: [params.get(Parameter.CUSTOM_DURATION)]) + "</b><br>"
             log.debug "Adding duration warning to welcome message, welcome is now: [" + welcome + "]"
         }
-
         welcome += "<br>" + message(code: "bigbluebutton.welcome.footer") + "<br>"
-
         String destinationURL = bigbluebuttonService.getJoinURL(params, welcome, ltiService.mode)
-        log.debug "redirecting to " + destinationURL
-
-        if( destinationURL != null ) {
-            redirect(url:destinationURL)
-        } else {
-            result.put("resultMessageKey", "BigBlueButtonServerError")
-            result.put("resultMessage", "The join could not be completed")
+        if (destinationURL == null) {
+            Map<String, String> result = new HashMap<String, String>()
+            result.put("messageKey", "BigBlueButtonServerError")
+            result.put("message", "The join could not be completed")
+            return result
         }
-
-        return result
+        log.debug "It is redirecting to " + destinationURL
+        redirect(url:destinationURL)
     }
 
     /**
@@ -258,14 +224,15 @@ class ToolController {
             if (key == "action" || key == "controller" || key == "format") {
                 // Ignore as these are the grails controller and action tied to this request.
                 continue
-            } else if (key == "oauth_signature") {
-                // We don't need this as part of the base string
+            }
+            if (key == "oauth_signature") {
+                // We don't need this as part of the base string.
                 continue
-            } else if (key == "request_method") {
-                // As this is was added by the controller, we don't want it as part of the base string
+            }
+            if (key == "request_method") {
+                // As this is was added by the controller, we don't want it as part of the base string.
                 continue
             }
-
             reqProp.setProperty(key, params.get(key));
         }
         return reqProp
@@ -279,24 +246,19 @@ class ToolController {
      */
     private boolean hasAllRequiredParams(Map<String, String> params, ArrayList<String> missingParams) {
         log.debug "Checking for required parameters"
-
-        boolean hasAllParams = true
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID) ) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID)) {
             missingParams.add(Parameter.CONSUMER_ID);
-            hasAllParams = false;
+            return false
         }
-
-        if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
+        if (ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
             missingParams.add(Parameter.OAUTH_SIGNATURE);
-            hasAllParams = false;
+            return false
         }
-
-        if ( !params.containsKey(Parameter.RESOURCE_LINK_ID) ) {
+        if (!params.containsKey(Parameter.RESOURCE_LINK_ID)) {
             missingParams.add(Parameter.RESOURCE_LINK_ID);
-            hasAllParams = false;
+            return false
         }
-
-        return hasAllParams
+        return true
     }
 
     /**
@@ -309,32 +271,23 @@ class ToolController {
      * @return - TRUE if the signatures matches the calculated signature
      */
     private boolean checkValidSignature(String method, String url, String conSecret, Properties postProp, String signature) {
-        def validSignature = false
-
-        if ( ltiService.hasRestrictedAccess() ) {
-            try {
-                OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
-                //log.debug "OAuthMessage oam = " + oam.toString()
-
-                HMAC_SHA1 hmac = new HMAC_SHA1()
-                //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
-
-                hmac.setConsumerSecret(conSecret)
-
-                log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
-                String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
-                log.debug "Calculated: " + calculatedSignature + " Received: " + signature
-
-                validSignature = calculatedSignature.equals(signature)
-            } catch( Exception e ) {
-                log.debug "Exception error: " + e.message
-            }
-
-        } else {
-            validSignature = true
+        if (!ltiService.hasRestrictedAccess()) {
+            return true;
+        }
+        try {
+            OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
+            //log.debug "OAuthMessage oam = " + oam.toString()
+            HMAC_SHA1 hmac = new HMAC_SHA1()
+            //log.debug "HMAC_SHA1 hmac = " + hmac.toString()
+            hmac.setConsumerSecret(conSecret)
+            log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
+            String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
+            log.debug "Calculated: " + calculatedSignature + " Received: " + signature
+            return calculatedSignature.equals(signature)
+        } catch( Exception e ) {
+            log.debug "Exception error: " + e.message
+            return false
         }
-
-        return validSignature
     }
 
     /**
@@ -343,20 +296,34 @@ class ToolController {
      * @return the key:val pairs needed for Basic LTI
      */
     private List<Object> getSanitizedRecordings(Map<String, String> params) {
-        List<Object> recordings = bigbluebuttonService.getRecordings(params)
-        for(Map<String, Object> recording: recordings){
-            /// Calculate duration
+        def recordings = new ArrayList<Object>()
+        def getRecordingsResponse = bigbluebuttonService.getRecordings(params)
+        if (getRecordingsResponse == null) {
+            return recordings
+        }
+        Object response = (Object)getRecordingsResponse.get("recording")
+        if (response instanceof Map<?,?>) {
+            recordings.add(response)
+        }
+        if (response instanceof Collection<?>) {
+            recordings = response
+        }
+        // Sanitize recordings
+        Iterator i = recordings.iterator();
+        while (i.hasNext()) {
+            def recording = i.next()
+            // Calculate duration.
             long endTime = Long.parseLong((String)recording.get("endTime"))
             endTime -= (endTime % 1000)
             long startTime = Long.parseLong((String)recording.get("startTime"))
             startTime -= (startTime % 1000)
             int duration = (endTime - startTime) / 60000
-            /// Add duration
+            // Add duration.
             recording.put("duration", duration )
-            /// Calculate reportDate
+            // Calculate reportDate.
             DateFormat df = new SimpleDateFormat(message(code: "tool.view.dateFormat"))
             String reportDate = df.format(new Date(startTime))
-            /// Add reportDate
+            // Add reportDate.
             recording.put("reportDate", reportDate)
             recording.put("unixDate", startTime / 1000)
         }
@@ -399,4 +366,9 @@ class ToolController {
 
         return cartridge
     }
+
+    private void renderError(key, message) {
+        log.debug "Error [resultMessageKey:'" + key + "', resultMessage:'" + message + "']"
+        render(view: "error", model: ['resultMessageKey': key, 'resultMessage': message])
+    }
 }
diff --git a/bbb-lti/grails-app/i18n/messages.properties b/bbb-lti/grails-app/i18n/messages.properties
index a9a014740524315d68b6ab5d1e78b046d2f031de..0385dd5f83b75874d4fdde0d3eb08f7b26093b74 100644
--- a/bbb-lti/grails-app/i18n/messages.properties
+++ b/bbb-lti/grails-app/i18n/messages.properties
@@ -17,7 +17,7 @@
 #
 
 # The welcome.header can be static, however if you want the name of the activity (meeting) to be injected use {0} as part of the text
-# {1} can be used to inject the name of the course 
+# {1} can be used to inject the name of the course
 bigbluebutton.welcome.header=Welcome to <b>{0}</b>!
 bigbluebutton.welcome.footer=To understand how BigBlueButton works see our <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the headset icon (upper-left hand corner). <b>Please use a headset to avoid causing noise for others.
 bigbluebutton.welcome.record=This meeting is being recorded
@@ -38,9 +38,10 @@ tool.view.recording.unpublish=Unpublish
 tool.view.recording.delete=Delete
 tool.view.activity=Activity
 tool.view.description=Description
+tool.view.preview=Preview
 tool.view.date=Date
 tool.view.duration=Duration
 tool.view.actions=Actions
 tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
 
-tool.error.general=Connection could not be established.
\ No newline at end of file
+tool.error.general=Connection could not be established.
diff --git a/bbb-lti/grails-app/i18n/messages_es.properties b/bbb-lti/grails-app/i18n/messages_es.properties
index 7e0eedbf335ba1e69dffed503faf3e860a44f3e0..b749c09d525a5fb43763ccb414654d6d5723af04 100644
--- a/bbb-lti/grails-app/i18n/messages_es.properties
+++ b/bbb-lti/grails-app/i18n/messages_es.properties
@@ -39,9 +39,10 @@ tool.view.recording.confirmation.yes=Si
 tool.view.recording.confirmation.no=No
 tool.view.activity=Actividad
 tool.view.description=Descripci&#243;n
+tool.view.preview=Vista preliminar
 tool.view.date=Fecha
 tool.view.duration=Duraci&#243;n
 tool.view.actions=Acciones
 tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
 
-tool.error.general=No pudo estableserce la conexi&#243;n.
\ No newline at end of file
+tool.error.general=No pudo estableserce la conexi&#243;n.
diff --git a/bbb-lti/grails-app/i18n/messages_fr.properties b/bbb-lti/grails-app/i18n/messages_fr.properties
index 70c9fd7fbda9b803d3c1ecbda3db82b568235148..6d56c9e0d0cf9d7f7d37af1474c0815632f13ee0 100644
--- a/bbb-lti/grails-app/i18n/messages_fr.properties
+++ b/bbb-lti/grails-app/i18n/messages_fr.properties
@@ -1,42 +1,43 @@
-#
-# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
-#
-# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
-#
-# This program is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free Software
-# Foundation; either version 3.0 of the License, or (at your option) any later
-# version.
-#
-# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
-# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-#
-
-bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
-bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
-
-tool.view.app=BigBlueButton
-tool.view.title=LTI Interface pour BigBlueButton
-tool.view.join=Saisie de la r&#233;union
-tool.view.recording=Enregistrement
-tool.view.recording.format.presentation=presentation
-tool.view.recording.format.video=video
-tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
-tool.view.recording.delete.confirmation.warning=Attention
-tool.view.recording.delete.confirmation.yes=Oui
-tool.view.recording.delete.confirmation.no=Non
-tool.view.recording.publish=Publier
-tool.view.recording.unpublish=D&#233;publier
-tool.view.recording.delete=Supprimer
-tool.view.activity=Activit&#233;
-tool.view.description=Description
-tool.view.date=Date
-tool.view.duration=Dur&#233;e
-tool.view.actions=Actions
-tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
-
-tool.error.general=Pas possible &#233;tablir la connection.
\ No newline at end of file
+#
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+#
+# Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+#
+
+bigbluebutton.welcome.header=<br>Bienvenue au <b>{0}</b>!<br>
+bigbluebutton.welcome.footer=<br>Pour comprendre comment fonctionne BigBlueButton, consultez les <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>didacticiels vid&#233;o</u></a>.<br><br>Pour activer l'audio cliquez sur l'ic&#244;ne du casque &#224; &#233;couteurs (coin sup&#233;rieur gauche). <b>S'il vous pla�t utiliser le casque pour &#233;viter de causer du bruit.</b>
+
+tool.view.app=BigBlueButton
+tool.view.title=LTI Interface pour BigBlueButton
+tool.view.join=Saisie de la r&#233;union
+tool.view.recording=Enregistrement
+tool.view.recording.format.presentation=presentation
+tool.view.recording.format.video=video
+tool.view.recording.delete.confirmation=Veillez &#224; supprimer d&#233;finitivement cet enregistrement?
+tool.view.recording.delete.confirmation.warning=Attention
+tool.view.recording.delete.confirmation.yes=Oui
+tool.view.recording.delete.confirmation.no=Non
+tool.view.recording.publish=Publier
+tool.view.recording.unpublish=D&#233;publier
+tool.view.recording.delete=Supprimer
+tool.view.activity=Activit&#233;
+tool.view.description=Description
+tool.view.preview=Apre&#231;u
+tool.view.date=Date
+tool.view.duration=Dur&#233;e
+tool.view.actions=Actions
+tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
+
+tool.error.general=Pas possible &#233;tablir la connection.
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
index 297a6ce55422c33579812f4de11ead16c2791793..17714737b5b13d625525ff043bdcfbd90ac3f930 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/BigbluebuttonService.groovy
@@ -34,6 +34,10 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.XML;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
@@ -63,20 +67,20 @@ class BigbluebuttonService {
         try {
             docBuilder = docBuilderFactory.newDocumentBuilder()
         } catch (ParserConfigurationException e) {
-            logger.error("Failed to initialise BaseProxy", e)
+            log.error("Failed to initialise BaseProxy", e)
         }
-
         //Instantiate bbbProxy and initialize it with default url and salt
         bbbProxy = new Proxy(url, salt)
     }
 
     public String getJoinURL(params, welcome, mode){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        String joinURL = null
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingName = getValidatedMeetingName(params.get(Parameter.RESOURCE_LINK_TITLE))
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
         String attendeePW = DigestUtils.shaHex("ap" + params.get(Parameter.RESOURCE_LINK_ID) + params.get(Parameter.CONSUMER_ID))
@@ -86,7 +90,6 @@ class BigbluebuttonService {
         String userFullName = getValidatedUserFullName(params, isModerator)
         String courseTitle = getValidatedCourseTitle(params.get(Parameter.COURSE_TITLE))
         String userID = getValidatedUserId(params.get(Parameter.USER_ID))
-
         Integer voiceBridge = 0
         String record = false
         Integer duration = 0
@@ -95,98 +98,93 @@ class BigbluebuttonService {
             record = getValidatedBBBRecord(params.get(Parameter.CUSTOM_RECORD)) || ltiService.allRecordedByDefault()
             duration = getValidatedBBBDuration(params.get(Parameter.CUSTOM_DURATION))
         }
-
         Boolean allModerators = Boolean.valueOf(false)
         if ( params.containsKey(Parameter.CUSTOM_ALL_MODERATORS) ) {
             allModerators = Boolean.parseBoolean(params.get(Parameter.CUSTOM_ALL_MODERATORS))
         }
-
         String[] values = [meetingName, courseTitle]
         String welcomeMsg = MessageFormat.format(welcome, values)
-
         String meta = getMonitoringMetaData(params)
-
-        String createURL = getCreateURL( meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta )
-        log.debug "createURL: " + createURL
-        Map<String, Object> createResponse = doAPICall(createURL)
-        log.debug "createResponse: " + createResponse
-
-        if( createResponse != null){
-            String returnCode = (String) createResponse.get("returncode")
-            String messageKey = (String) createResponse.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
-                (Proxy.APIRESPONSE_FAILED.equals(returnCode) &&  (Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) || Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey)) ) ){
-                joinURL = bbbProxy.getJoinURL( userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) createResponse.get("createTime"), userID);
-            }
+        String createURL = getCreateURL(meetingName, meetingID, attendeePW, moderatorPW, welcomeMsg, voiceBridge, logoutURL, record, duration, meta)
+        Map<String, Object> responseAPICall = doAPICall(createURL)
+        log.info "responseAPICall: " + responseAPICall
+        if (responseAPICall == null) {
+            return null
         }
-
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) ||
+            !Proxy.MESSAGEKEY_IDNOTUNIQUE.equals(messageKey) &&
+            !Proxy.MESSAGEKEY_DUPLICATEWARNING.equals(messageKey) &&
+            !"".equals(messageKey)) {
+            return null
+        }
+        def joinURL = bbbProxy.getJoinURL(userFullName, meetingID, (isModerator || allModerators)? moderatorPW: attendeePW, (String) response.get("createTime"), userID)
+        log.info "joinURL: " + joinURL
         return joinURL
     }
 
-    public Object getRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
+    public Object getRecordings(params) {
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String meetingID = getValidatedMeetingId(params.get(Parameter.RESOURCE_LINK_ID), params.get(Parameter.CONSUMER_ID))
-
-        String recordingsURL = bbbProxy.getGetRecordingsURL( meetingID )
-        log.debug "recordingsURL: " + recordingsURL
-        Map<String, Object> recordings = doAPICall(recordingsURL)
-
-        if( recordings != null){
-            String returnCode = (String) recordings.get("returncode")
-            String messageKey = (String) recordings.get("messageKey")
-            if ( Proxy.APIRESPONSE_SUCCESS.equals(returnCode) && messageKey == null ){
-                return recordings.get("recordings")
-            }
+        String recordingsURL = bbbProxy.getGetRecordingsURL(meetingID)
+        Map<String, Object> responseAPICall = doAPICall(recordingsURL)
+        if (responseAPICall == null) {
+            return null
         }
-
-        return null
+        Object response = (Object)responseAPICall.get("response")
+        String returnCode = (String)response.get("returncode")
+        String messageKey = (String)response.get("messageKey")
+        if (!Proxy.APIRESPONSE_SUCCESS.equals(returnCode) || messageKey != null) {
+            return null
+        }
+        Object recordings = (Object)response.get("recordings")
+        return recordings
     }
 
     public Object doDeleteRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
-
-        if( !recordingId.equals("") ){
+        if (!recordingId.equals("")) {
             String deleteRecordingsURL = bbbProxy.getDeleteRecordingsURL( recordingId )
-            log.debug "deleteRecordingsURL: " + deleteRecordingsURL
-            result = doAPICall(deleteRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(deleteRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
     public Object doPublishRecordings(params){
-        //Set the injected values
-        if( !url.equals(bbbProxy.url) && !url.equals("") ) bbbProxy.setUrl(url)
-        if( !salt.equals(bbbProxy.salt) && !salt.equals("") ) bbbProxy.setSalt(salt)
-
-        Map<String, Object> result
-
+        // Set the injected values
+        if (!url.equals(bbbProxy.url) && !url.equals("")) {
+            bbbProxy.setUrl(url)
+        }
+        if (!salt.equals(bbbProxy.salt) && !salt.equals("")) {
+            bbbProxy.setSalt(salt)
+        }
         String recordingId = getValidatedBBBRecordingId(params.get(Parameter.BBB_RECORDING_ID))
         String publish = getValidatedBBBRecordingPublished(params.get(Parameter.BBB_RECORDING_PUBLISHED))
-
         if( !recordingId.equals("") ){
             String publishRecordingsURL = bbbProxy.getPublishRecordingsURL( recordingId, "true".equals(publish)?"false":"true" )
-            log.debug "publishRecordingsURL: " + publishRecordingsURL
-            result = doAPICall(publishRecordingsURL)
-        } else {
-            result = new HashMap<String, String>()
-            result.put("resultMessageKey", "InvalidRecordingId")
-            result.put("resultMessage", "RecordingId is invalid. The recording can not be deleted.")
+            return doAPICall(publishRecordingsURL)
         }
-
+        def result = new HashMap<String, String>()
+        result.put("messageKey", "InvalidRecordingId")
+        result.put("message", "RecordingId is invalid. The recording can not be deleted.")
         return result
     }
 
@@ -219,14 +217,14 @@ class BigbluebuttonService {
         String userFirstName = params.get(Parameter.USER_FIRSTNAME)
         String userLastName = params.get(Parameter.USER_LASTNAME)
         if( userFullName == null || userFullName == "" ){
-            if( userFirstName != null && userFirstName != "" ){
+            if (userFirstName != null && userFirstName != "") {
                 userFullName = userFirstName
             }
-            if( userLastName != null && userLastName != "" ){
+            if (userLastName != null && userLastName != "") {
                 userFullName += userFullName.length() > 0? " ": ""
                 userFullName += userLastName
             }
-            if( userFullName == null || userFullName == "" ){
+            if (userFullName == null || userFullName == "") {
                 userFullName = isModerator? "Moderator" : "Attendee"
             }
         }
@@ -263,8 +261,7 @@ class BigbluebuttonService {
 
     private String getMonitoringMetaData(params){
         String meta
-
-        meta = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
+        meta  = "meta_origin=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_CODE) == null? "": params.get(Parameter.TOOL_CONSUMER_CODE))
         meta += "&meta_originVersion=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_VERSION) == null? "": params.get(Parameter.TOOL_CONSUMER_VERSION))
         meta += "&meta_originServerCommonName=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_DESCRIPTION))
         meta += "&meta_originServerUrl=" + bbbProxy.getStringEncoded(params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL) == null? "": params.get(Parameter.TOOL_CONSUMER_INSTANCE_URL))
@@ -272,25 +269,21 @@ class BigbluebuttonService {
         meta += "&meta_contextId=" + bbbProxy.getStringEncoded(params.get(Parameter.COURSE_ID) == null? "": params.get(Parameter.COURSE_ID))
         meta += "&meta_contextActivity=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_TITLE) == null? "": params.get(Parameter.RESOURCE_LINK_TITLE))
         meta += "&meta_contextActivityDescription=" + bbbProxy.getStringEncoded(params.get(Parameter.RESOURCE_LINK_DESCRIPTION) == null? "": params.get(Parameter.RESOURCE_LINK_DESCRIPTION))
-
         return meta
     }
 
     /** Make an API call */
     private Map<String, Object> doAPICall(String query) {
         StringBuilder urlStr = new StringBuilder(query);
-
         try {
             // open connection
-            //log.debug("doAPICall.call: " + query );
-
+            log.debug("doAPICall.call: " + query );
             URL url = new URL(urlStr.toString());
             HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
             httpConnection.setUseCaches(false);
             httpConnection.setDoOutput(true);
             httpConnection.setRequestMethod("GET");
             httpConnection.connect();
-
             int responseCode = httpConnection.getResponseCode();
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 // read response
@@ -302,35 +295,27 @@ class BigbluebuttonService {
                     reader = new BufferedReader(isr);
                     String line = reader.readLine();
                     while (line != null) {
-                        if( !line.startsWith("<?xml version=\"1.0\"?>"))
+                        if( !line.startsWith("<?xml version=\"1.0\"?>")) {
                             xml.append(line.trim());
+                        }
                         line = reader.readLine();
                     }
                 } finally {
-                    if (reader != null)
+                    if (reader != null) {
                         reader.close();
-                    if (isr != null)
+                    }
+                    if (isr != null) {
                         isr.close();
+                    }
                 }
                 httpConnection.disconnect();
-
-                // parse response
+                // Parse response.
                 //log.debug("doAPICall.responseXml: " + xml);
                 //Patch to fix the NaN error
                 String stringXml = xml.toString();
                 stringXml = stringXml.replaceAll(">.\\s+?<", "><");
-
-                Document dom = null;
-                dom = docBuilder.parse(new InputSource( new StringReader(stringXml)));
-
-                Map<String, Object> response = getNodesAsMap(dom, "response");
-                //log.debug("doAPICall.responseMap: " + response);
-
-                String returnCode = (String) response.get("returncode");
-                if (Proxy.APIRESPONSE_FAILED.equals(returnCode)) {
-                    log.debug("doAPICall." + (String) response.get("messageKey") + ": Message=" + (String) response.get("message"));
-                }
-
+                JSONObject rootJSON = XML.toJSONObject(stringXml);
+                Map<String, Object> response = jsonToMap(rootJSON);
                 return response;
             } else {
                 log.debug("doAPICall.HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode);
@@ -346,43 +331,43 @@ class BigbluebuttonService {
         }
     }
 
-    /** Get all nodes under the specified element tag name as a Java map */
-    protected Map<String, Object> getNodesAsMap(Document dom, String elementTagName) {
-        Node firstNode = dom.getElementsByTagName(elementTagName).item(0);
-        return processNode(firstNode);
+    protected Map<String, Object> jsonToMap(JSONObject json) throws JSONException {
+        Map<String, Object> retMap = new HashMap<String, Object>();
+        if(json != JSONObject.NULL) {
+            retMap = toMap(json);
+        }
+        return retMap;
     }
 
-    protected Map<String, Object> processNode(Node _node) {
+    protected Map<String, Object> toMap(JSONObject object) throws JSONException {
         Map<String, Object> map = new HashMap<String, Object>();
-        NodeList responseNodes = _node.getChildNodes();
-        for (int i = 0; i < responseNodes.getLength(); i++) {
-            Node node = responseNodes.item(i);
-            String nodeName = node.getNodeName().trim();
-            if (node.getChildNodes().getLength() == 1
-                    && ( node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.TEXT_NODE || node.getChildNodes().item(0).getNodeType() == org.w3c.dom.Node.CDATA_SECTION_NODE) ) {
-                String nodeValue = node.getTextContent();
-                map.put(nodeName, nodeValue != null ? nodeValue.trim() : null);
-
-            } else if (node.getChildNodes().getLength() == 0
-                    && node.getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE) {
-                map.put(nodeName, "");
-
-            } else if ( node.getChildNodes().getLength() >= 1
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.TEXT_NODE
-                    && node.getChildNodes().item(0).getChildNodes().item(0).getNodeType() != org.w3c.dom.Node.CDATA_SECTION_NODE ) {
-
-                List<Object> list = new ArrayList<Object>();
-                for (int c = 0; c < node.getChildNodes().getLength(); c++) {
-                    Node n = node.getChildNodes().item(c);
-                    list.add(processNode(n));
-                }
-                map.put(nodeName, list);
-
-            } else {
-                map.put(nodeName, processNode(node));
+        Iterator<String> keysItr = object.keys();
+        while(keysItr.hasNext()) {
+            String key = keysItr.next();
+            Object value = object.get(key);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
             }
+            map.put(key, value);
         }
         return map;
     }
+
+    protected List<Object> toList(JSONArray array) throws JSONException {
+        List<Object> list = new ArrayList<Object>();
+        for(int i = 0; i < array.length(); i++) {
+            Object value = array.get(i);
+            if(value instanceof JSONArray) {
+                value = toList((JSONArray) value);
+            }
+            else if(value instanceof JSONObject) {
+                value = toMap((JSONObject) value);
+            }
+            list.add(value);
+        }
+        return list;
+    }
 }
diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
index c78c127ba0eecd608ccd347761b02c4cb495a86b..2465c8e330e1c949e5d50538ae8eb8f0aaa38fd9 100644
--- a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
+++ b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy
@@ -46,42 +46,37 @@ class LtiService {
 
     private Map<String, String> getConsumer(consumerId) {
         Map<String, String> consumer = null
-
-        if( this.consumerMap.containsKey(consumerId) ){
+        if (this.consumerMap.containsKey(consumerId)) {
             consumer = new HashMap<String, String>()
             consumer.put("key", consumerId);
             consumer.put("secret",  this.consumerMap.get(consumerId))
         }
-
         return consumer
     }
 
-    private void initConsumerMap(){
+    private void initConsumerMap() {
         this.consumerMap = new HashMap<String, String>()
         String[] consumers = this.consumers.split(",")
-        //for( int i=0; i < consumers.length; i++){
-        if ( consumers.length > 0 ){
+        if ( consumers.length > 0 ) {
             int i = 0;
             String[] consumer = consumers[i].split(":")
             if( consumer.length == 2 ){
                 this.consumerMap.put(consumer[0], consumer[1])
             }
         }
-
     }
 
-    public String sign(String sharedSecret, String data) throws Exception
-    {
+    public String sign(String sharedSecret, String data)
+        throws Exception {
         Mac mac = setKey(sharedSecret)
-
         // Signed String must be BASE64 encoded.
         byte[] signBytes = mac.doFinal(data.getBytes("UTF8"));
         String signature = encodeBase64(signBytes);
         return signature;
     }
 
-    private Mac setKey(String sharedSecret) throws Exception
-    {
+    private Mac setKey(String sharedSecret)
+        throws Exception {
         Mac mac = Mac.getInstance("HmacSHA1");
         byte[] keyBytes = sharedSecret.getBytes("UTF8");
         SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA1");
@@ -110,7 +105,6 @@ class LtiService {
 
     def boolean isSSLEnabled(String query) {
         def ssl_enabled = false
-
         log.debug("Pinging SSL connection")
         try {
             // open connection
@@ -122,14 +116,12 @@ class LtiService {
             httpConnection.setRequestMethod("HEAD")
             httpConnection.setConnectTimeout(5000)
             httpConnection.connect()
-
             int responseCode = httpConnection.getResponseCode()
             if (responseCode == HttpURLConnection.HTTP_OK) {
                 ssl_enabled = true
             } else {
                 log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
             }
-
         } catch(IOException e) {
             log.debug("IOException: Message=" + e.getMessage())
         } catch(IllegalArgumentException e) {
@@ -148,4 +140,8 @@ class LtiService {
     def boolean allRecordedByDefault() {
         return Boolean.parseBoolean(this.recordedByDefault);
     }
+
+    def String getScheme(request) {
+        return request.isSecure() ? "https" : "http"
+    }
 }
diff --git a/bbb-lti/grails-app/views/tool/error.gsp b/bbb-lti/grails-app/views/tool/error.gsp
index e9cc755530f13de18d75ac2315554e6b4db7f9e6..e1442fa84d478f588c0a4b96420ae898ef3d0d3c 100644
--- a/bbb-lti/grails-app/views/tool/error.gsp
+++ b/bbb-lti/grails-app/views/tool/error.gsp
@@ -1,35 +1,35 @@
-<html>
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
-    <title>Error</title>
-    <asset:stylesheet src="bootstrap.css"/>
-    <asset:stylesheet src="tool.css"/>
-    <asset:javascript src="jquery.js"/>
-    <asset:javascript src="bootstrap.js"/>
-  </head>
-  <body>
-    <div class="body">
-      <br/><br/>
-      <div class="container">
-      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
-        <div class="alert alert-warning">
-          ${resultMessage}
-        </div>
-      </g:if>
-      <g:else>
-        <div class="alert alert-danger">
-          <g:message code="tool.error.general" />
-        </div>
-      </g:else>
-      </div>
-    </div>
-    <!-- {
-            "error": {
-                "messageKey": "${resultMessageKey}",
-                "message": "${resultMessage}"
-            }
-        }
-    -->
-    <br/><br/>
-  </body>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+    <title>Error</title>
+    <asset:stylesheet src="bootstrap.css"/>
+    <asset:stylesheet src="tool.css"/>
+    <asset:javascript src="jquery.js"/>
+    <asset:javascript src="bootstrap.js"/>
+  </head>
+  <body>
+    <div class="body">
+      <br/><br/>
+      <div class="container">
+      <g:if test="${ (resultMessageKey == 'InvalidEPortfolioUserId')}">
+        <div class="alert alert-warning">
+          ${resultMessage}
+        </div>
+      </g:if>
+      <g:else>
+        <div class="alert alert-danger">
+          <g:message code="tool.error.general" />
+        </div>
+      </g:else>
+      </div>
+    </div>
+    <!-- {
+            "error": {
+                "messageKey": "${resultMessageKey}",
+                "message": "${resultMessage}"
+            }
+        }
+    -->
+    <br/><br/>
+  </body>
 </html>
\ No newline at end of file
diff --git a/bbb-lti/grails-app/views/tool/index.gsp b/bbb-lti/grails-app/views/tool/index.gsp
index 342e5a52848c26a9013be97c83b51501be5ba544..c6b8526b94f0cac1beaa7237b3364ad39067c339 100644
--- a/bbb-lti/grails-app/views/tool/index.gsp
+++ b/bbb-lti/grails-app/views/tool/index.gsp
@@ -1,77 +1,79 @@
-<html>
-    <head>
-        <title><g:message code="tool.view.title" /></title>
-        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
-        <asset:stylesheet src="bootstrap.css"/>
-        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
-        <asset:javascript src="jquery.js"/>
-        <asset:javascript src="jquery.dataTables.min.js"/>
-        <asset:javascript src="dataTables.bootstrap.min.js"/>
-        <asset:javascript src="dataTables.plugin.datetime.js"/>
-        <asset:javascript src="moment-with-locales.min.js"/>
-        <asset:javascript src="bootstrap.js"/>
-        <asset:javascript src="bootstrap-confirmation.min.js"/>
-        <asset:javascript src="tool.js"/>
-    </head>
-    <body>
-        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
-        <br><br>
-        <div class="container">
-        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
-            <thead>
-                <tr>
-                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
-                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
-                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
-                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
-                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
-                    <g:if test="${ismoderator}">
-                    <th class="header c5 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
-                    </g:if>
-                </tr>
-            </thead>
-            <tbody>
-            <g:each in="${recordingList}" var="r">
-                <g:if test="${ismoderator || r.published == 'true'}">  
-                <tr class="r0 lastrow">
-                    <td class="cell c0" style="text-align:center;">
-                    <g:if test="${r.published == 'true'}">
-                    <g:each in="${r.playback}" var="p">
-                        <a title="<g:message code="tool.view.recording.format.${p.type}" />" target="_new" href="${p.url}"><g:message code="tool.view.recording.format.${p.type}" /></a>&#32;
-                    </g:each>
-                    </g:if>
-                    </td>
-                    <td class="cell c1" style="text-align:center;">${r.name}</td>
-                    <td class="cell c2" style="text-align:center;">${r.metadata.contextactivitydescription}</td>
-                    <td class="cell c3" style="text-align:center;">${r.unixDate}</td>
-                    <td class="cell c4" style="text-align:center;">${r.duration}</td>
-                    <g:if test="${ismoderator}">
-                    <td class="cell c5 lastcol" style="text-align:center;">
-                      <g:if test="${r.published == 'true'}">
-                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:if>
-                      <g:else>
-                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
-                      </g:else>
-                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
-                        data-toggle="confirmation"
-                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
-                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
-                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
-                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
-                        data-placement="left"
-                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
-                      </a>
-                    </td>
-                    </g:if>
-                </tr>
-                </g:if>
-            </g:each>
-            </tbody>
-        </table>
-        </div>
-    </body>
-    <g:javascript>
-        var locale = '${params.launch_presentation_locale}';
-    </g:javascript>  
-</html>
\ No newline at end of file
+<html>
+    <head>
+        <title><g:message code="tool.view.title" /></title>
+        <link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
+        <asset:stylesheet src="bootstrap.css"/>
+        <asset:stylesheet src="dataTables.bootstrap.min.css"/>
+        <asset:javascript src="jquery.js"/>
+        <asset:javascript src="jquery.dataTables.min.js"/>
+        <asset:javascript src="dataTables.bootstrap.min.js"/>
+        <asset:javascript src="dataTables.plugin.datetime.js"/>
+        <asset:javascript src="moment-with-locales.min.js"/>
+        <asset:javascript src="bootstrap.js"/>
+        <asset:javascript src="bootstrap-confirmation.min.js"/>
+        <asset:javascript src="tool.js"/>
+    </head>
+    <body>
+        <h1 style="margin-left:20px; text-align: center;"><a title="<g:message code="tool.view.join" />" class="btn btn-primary btn-large" href="${createLink(controller:'tool', action:'join', id: '0')}"><g:message code="tool.view.join" /></a></h1>
+        <br><br>
+        <div class="container">
+        <table id="recordings" class="table table-striped table-bordered dt-responsive" width="100%">
+            <thead>
+                <tr>
+                    <th class="header c0" style="text-align:center;" scope="col"><g:message code="tool.view.recording" /></th>
+                    <th class="header c1" style="text-align:center;" scope="col"><g:message code="tool.view.activity" /></th>
+                    <th class="header c2" style="text-align:center;" scope="col"><g:message code="tool.view.description" /></th>
+                    <th class="header c3" style="text-align:center;" scope="col"><g:message code="tool.view.preview" /></th>
+                    <th class="header c4" style="text-align:center;" scope="col"><g:message code="tool.view.date" /></th>
+                    <th class="header c5" style="text-align:center;" scope="col"><g:message code="tool.view.duration" /></th>
+                    <g:if test="${ismoderator}">
+                    <th class="header c6 lastcol" style="text-align:center;" scope="col"><g:message code="tool.view.actions" /></th>
+                    </g:if>
+                </tr>
+            </thead>
+            <tbody>
+            <g:each in="${recordingList}" var="r">
+                <g:if test="${ismoderator || r.published == 'true'}">
+                <tr class="r0 lastrow">
+                    <td class="cell c0" style="text-align:center;">
+                    <g:if test="${r.published}">
+                        <g:each in="${r.playback}" var="format">
+                            <a title="<g:message code="tool.view.recording.format.${format.getValue().type}" />" target="_new" href="${format.getValue().url}"><g:message code="tool.view.recording.format.${format.getValue().type}" /></a>&#32;
+                        </g:each>
+                    </g:if>
+                    </td>
+                    <td class="cell c1" style="text-align:left;">${r.name}</td>
+                    <td class="cell c2" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
+                    <td class="cell c3" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
+                    <td class="cell c4" style="text-align:left;">${r.reportDate}</td>
+                    <td class="cell c5" style="text-align:right;">${r.duration}</td>
+                    <g:if test="${ismoderator}">
+                    <td class="cell c6 lastcol" style="text-align:center;">
+                      <g:if test="${r.published == 'true'}">
+                      <a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:if>
+                      <g:else>
+                      <a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
+                      </g:else>
+                      <a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
+                        data-toggle="confirmation"
+                        data-title="<g:message code="tool.view.recording.delete.confirmation.warning" />"
+                        data-content="<g:message code="tool.view.recording.delete.confirmation" />"
+                        data-btn-ok-label="<g:message code="tool.view.recording.delete.confirmation.yes" />"
+                        data-btn-cancel-label="<g:message code="tool.view.recording.delete.confirmation.no" />"
+                        data-placement="left"
+                        href="${createLink(controller:'tool',action:'delete',id: '0')}?bbb_recording_id=${r.recordID}">
+                      </a>
+                    </td>
+                    </g:if>
+                </tr>
+                </g:if>
+            </g:each>
+            </tbody>
+        </table>
+        </div>
+    </body>
+    <g:javascript>
+        var locale = '${params.launch_presentation_locale}';
+    </g:javascript>
+</html>