miércoles, 24 de mayo de 2017

Migration to Grails 3

After many years with no posting I dare creating my first entry in English. A few months ago I was playing around Grails 3 migration from a Grails 2.3.11 project and these were my steps to finish the process:
  • Create a new project via Intellij with Grails 3.2.9 and Java 8
grails create-app education

When executing create-app command, by default the "web" profile is used.
  • Place all files from Grails 2 project to Grails 3 project respecting packages
Old LocationNew LocationDescription
grails-app/conf/BuildConfig.groovybuild.gradleBuild time configuration is now defined in a Gradle build file
grails-app/conf/Config.groovygrails-app/conf/application.groovyRenamed for consistency with Spring Boot
grails-app/conf/UrlMappings.groovygrails-app/controllers/UrlMappings.groovyMoved since grails-app/conf is not a source directory anymore
grails-app/conf/BootStrap.groovygrails-app/init/BootStrap.groovyMoved since grails-app/conf is not a source directory anymore
scriptssrc/main/scriptsMoved for consistency with Gradle
src/groovysrc/main/groovyMoved for consistency with Gradle
src/javasrc/main/groovy (yes, groovy!)Moved for consistency with Gradle
test/unitsrc/test/groovyMoved for consistency with Gradle
test/integrationsrc/integration-test/groovyMoved for consistency with Gradle
web-app
src/main/webapp or src/main/resources/
Moved for consistency with Gradle
\*GrailsPlugin.groovysrc/main/groovyThe plugin descriptor moved to a source directory

src/main/resources/public is recommended as src/main/webapp only gets included in WAR packaging but not in JAR packaging.
It is recommended to merge Java source files from src/java into src/main/groovy. You can create a src/main/java directory if you want to and it will be used but it is generally better to combine the folders. (The Groovy and Java sources compile together.)
  • Our BuildConfig.groovy in Grails 2 project will be our build.gradle in Grails 3 project
buildscript {
   repositories {
     mavenLocal()
     maven { url "https://repo.grails.org/grails/core" }
   }
   dependencies {
     classpath "org.grails:grails-gradle-plugin:$grailsVersion"
     classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.1"
     classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
     classpath "org.grails.plugins:database-migration:3.0.0"
   }
}

version "1.1"
group "education"

apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.grails-gsp"
apply plugin:"asset-pipeline"

repositories {
   mavenLocal()
   maven { url "https://repo.grails.org/grails/core" }
}

dependencies {
   compile "org.springframework.boot:spring-boot-starter-logging"
   compile "org.springframework.boot:spring-boot-autoconfigure"
   compile "org.grails:grails-core"
   compile "org.springframework.boot:spring-boot-starter-actuator"
   compile "org.springframework.boot:spring-boot-starter-tomcat"
   compile "org.grails:grails-dependencies"
   compile "org.grails:grails-web-boot"
   compile "org.grails.plugins:cache"
   compile "org.grails.plugins:scaffolding"
   compile "org.grails.plugins:hibernate5"
   compile "org.hibernate:hibernate-core:5.1.3.Final"
   compile "org.hibernate:hibernate-ehcache:5.1.3.Final"
   console "org.grails:grails-console"
   profile "org.grails.profiles:web"
   runtime "com.bertramlabs.plugins:asset-pipeline-grails:2.14.1"
   runtime "com.h2database:h2"
   testCompile "org.grails:grails-plugin-testing"
   testCompile "org.grails.plugins:geb"
   testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
   testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"

   // MySQL 5 Driver
   runtime "mysql:mysql-connector-java:5.1.36"

   // Ajax Tags
   compile "org.grails.plugins:ajax-tags:1.0.1.BUILD-SNAPSHOT"

   // Joda Time
   compile "joda-time:joda-time:2.9.6"

   // Rest Client Builder
   compile "org.grails:grails-datastore-rest-client"

   // Database Migration
   compile "org.grails.plugins:database-migration:3.0.0"
   compile "org.liquibase:liquibase-core:3.5.3"

   // Spring Security Core
   compile "org.grails.plugins:spring-security-core:3.1.1"

   // Spring Security ACL
   compile "org.grails.plugins:spring-security-acl:3.1.0"

   // Spring Security Rest
   //compile "org.grails.plugins:spring-security-rest:2.0.0.M2"

   // Email
   compile "org.grails.plugins:mail:2.0.0.RC6"

   // Export
   compile "org.grails.plugins:export:2.0.0"

   // Google Visualization
   compile "org.grails.plugins:grails-google-visualization:2.2"

   // CKEditor
   compile "org.grails.plugins:ckeditor:4.5.9.0"
}

bootRun {
   jvmArgs('-Dspring.output.ansi.enabled=always')
   addResources = true
}

sourceSets {
   main {
     resources {
       srcDir 'grails-app/migrations'
     }
   }
}

assets {
   minifyJs = true
   minifyCss = true
}

There will be plugins already created for Grails 3 distribution but others don't. For these ones we must look for a workaround:
  1. Convert them by ourselves to Grails 3
  2. Get rid of them and implement them by code
  • Our Datasource.groovy in Grails 2 project will be in our application.yml in Grails 3 project
---
grails:
    profile: web
    codegen:
        defaultPackage: net.rdcstudios.core
    spring:
        transactionManagement:
            proxies: false
    gorm:
        # Whether to autowire entities. 
        # Disabled by default for performance reasons.
        autowire: true # Changed to true to be able to inject services into domain classes         
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
info:
    app:
        name: '@info.app.name@'
        version: '@info.app.version@'
        grailsVersion: '@info.app.grailsVersion@'
spring:
    main:
        banner-mode: "off"
    groovy:
        template:
            check-template-location: false

# Spring Actuator Endpoints are Disabled by Default
endpoints:
    enabled: false
    jmx:
        enabled: true

---
grails:
    mime:
        disable:
            accept:
                header:
                    userAgents:
                        - Gecko
                        - WebKit
                        - Presto
                        - Trident
        types:
            all: '*/*'
            atom: application/atom+xml
            css: text/css
            csv: text/csv
            form: application/x-www-form-urlencoded
            html:
              - text/html
              - application/xhtml+xml
            js: text/javascript
            json:
              - application/json
              - text/json
            multipartForm: multipart/form-data
            pdf: application/pdf
            rss: application/rss+xml
            text: text/plain
            hal:
              - application/hal+json
              - application/hal+xml
            xml:
              - text/xml
              - application/xml
    urlmapping:
        cache:
            maxsize: 1000
    controllers:
        defaultScope: singleton
    converters:
        encoding: UTF-8
    views:
        default:
            codec: html
        gsp:
            encoding: UTF-8
            htmlcodec: xml
            codecs:
                expression: html
                scriptlets: html
                taglib: none
                staticparts: none
endpoints:
    jmx:
        unique-names: true

---
hibernate:
    cache:
        queries: false
        use_second_level_cache: true
        use_query_cache: false
        region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory

dataSource:
    pooled: true
    jmxExport: true
    driverClassName: com.mysql.jdbc.Driver
    dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    # dialect: net.rdcstudios.mysql.dialect.MySQLUTF8MB4InnoDBDialect
    username: core
    password: core

environments:
    development:
            dataSource:
                dbCreate: none
    internaldemo:
            dataSource:
                dbCreate: none
    demo:
            dataSource:
                dbCreate: none
    client:
            dataSource:
                dbCreate: none
    test:
            dataSource:
                dbCreate: create-drop
    uat:
            dataSource:
                dbCreate: none
    production:
        dataSource:
            dbCreate: none
            properties:
                jmxEnabled: true
                initialSize: 5
                maxActive: 50
                minIdle: 5
                maxIdle: 25
                maxWait: 10000
                maxAge: 600000
                timeBetweenEvictionRunsMillis: 5000
                minEvictableIdleTimeMillis: 60000
                validationQuery: SELECT 1
                validationQueryTimeout: 3
                validationInterval: 15000
                testOnBorrow: true
                testWhileIdle: true
                testOnReturn: false
                jdbcInterceptors: ConnectionState
                defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED

  • Our log4j which is inside Config.groovy in Grails 2 project will be our logback.groovy in Grails 3 project
import grails.util.BuildSettings
import grails.util.Environment
import org.springframework.boot.logging.logback.ColorConverter
import org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter

conversionRule 'clr', ColorConverter
conversionRule 'wex', WhitespaceThrowableProxyConverter

// See http://logback.qos.ch/manual/groovy.html for details on configuration
//statusListener(OnConsoleStatusListener)

def appenderList = ["ROLLING"]
def LOG_DIR = "."
def consoleAppender = true

def targetDir = BuildSettings.TARGET_DIR

if (targetDir != null) {
    appenderList.add("CONSOLE")
    LOG_DIR = "/var/log/education/${Environment.currentEnvironment.name}"
} else {
    LOG_DIR = "/var/log/education/${Environment.currentEnvironment.name}"
    consoleAppender = false
}

if (consoleAppender) {
    appender("CONSOLE", ConsoleAppender) {
        encoder(PatternLayoutEncoder) {
            pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
        }
    }
}

appender("ROLLING", RollingFileAppender) {
    encoder(PatternLayoutEncoder) {
        Pattern = "%d %level %thread %mdc %logger - %m%n"
    }
    rollingPolicy(SizeAndTimeBasedRollingPolicy) {
        MaxFileSize = "5MB"
        FileNamePattern = "${LOG_DIR}/log.%d{yyyy-MM-dd}.%i"
    }
}

logger 'grails.app.services', INFO, appenderList
logger 'grails.app.controllers', INFO, appenderList
logger 'net.rdcstudios.core', INFO, appenderList

//root(INFO, appenderList)

// Spring Security
//logger 'grails.plugin.springsecurity.web.filter.DebugFilter', INFO, ['STDOUT']

// Hibernate Logs
//logger 'org.hibernate.SQL', DEBUG, ['STDOUT']
//logger 'org.hibernate.type.descriptor.sql.BasicBinder', TRACE, ['STDOUT']
  • In Grails 3.x all internal APIs can be found in the org.grails package and public facing APIs in the grails package. The org.codehaus.groovy.grails package no longer exists. All package declaration in sources should be modified for the new location of the respective classes. Examples
org.codehaus.groovy.grails.commons.GrailsApplication --> grails.core.GrailsApplication
org.codehaus.groovy.grails.core.io.ResourceLocator --> org.grails.core.io.ResourceLocator
org.codehaus.groovy.grails.web.mapping.LinkGenerator --> grails.web.mapping.LinkGenerator
org.codehaus.groovy.grails.web.binding.DataBindingUtils --> grails.web.databinding.DataBindingUtils
org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap --> grails.web.servlet.mvc.GrailsParameterMap

In unit or integration tests...
org.codehaus.groovy.grails.plugins.testing.GrailsMockMultipartFile --> org.grails.plugins.testing.GrailsMockMultipartFile
  • As of now Grails 3 uses Pipeline Plugin to work with assets instead of Resource Plugin in Grails 2. Therefore, we have to move all assets:
    1. web-app/css --> grails-app/assets/stylesheets
    2. web-app/images --> grails-app/assets/images
    3. web-app/js --> grails-app/assets/javascripts
  • Create application.js as an index of all js files
// This is a manifest file that'll be compiled into application.js.
//
// Any JavaScript file within this directory can be referenced here using a relative path.
//
// You're free to add application-wide JavaScript to this file, but it's generally better
// to create separate JavaScript files as needed.
//
//= require jquery-2.2.0.min
//= require ie10-viewport-bug-workaround
//= require rs-plugin/js/jquery.themepunch.tools.min
//= require rs-plugin/js/jquery.themepunch.revolution.min
//= require custom
//= require menu/jquery.mmenu.min.all
//= require bootstrap/bootstrap.min
//= require bootstrap/moment
//= require bootstrap/bootstrap-datetimepicker
//= require bootstrap/bootstrap-switch.min
//= require bootstrap/bootstrap-wizard
//= require bootstrap/bootstrap-combobox
//= require fullcalendar/fullcalendar.min
//= require fullcalendar/lang-all
//= require_self

if (typeof jQuery !== 'undefined') {
    (function($) {
        $(document).ajaxStart(function() {
            $('#spinner').fadeIn();
        }).ajaxStop(function() {
            $('#spinner').fadeOut();
        });
    })(jQuery);
}
  • Create application.css as an index of all css files
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS file within this directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the top of the
* compiled file, but it's generally better to create a new file per style scope.
*
*= encoding UTF-8
*= require bootstrap/bootstrap-responsive
*= require pure-drawer
*= require style
*= require font-awesome/css/font-awesome.min
*= require menu/jquery.mmenu.all
*= require rs-plugin/css/extralayers
*= require rs-plugin/css/settings
*= require bootstrap/bootstrap-datetimepicker
*= require bootstrap/bootstrap-switch.min
*= require bootstrap/bootstrap-wizard
*= require bootstrap/bootstrap-combobox
*= require fullcalendar/fullcalendar.min
*= require_self
*/
  • Add application.js and application.css to main.gsp
<asset:stylesheet src="application.css"/>
<asset:javascript src="application.js"/>
  • Change all <img> and <g:img> to <asset:image src="" />
  • Change all <script>, <g:script> and <r:script> to <asset:script type="text/javascript">
  • I had to add this line just before </body> in the outer main template to make javascript code embedded in views or templates work
 <asset:deferredScripts/>
  • Change all ${resource()} methods to ${assetPath(src: ''}
  • Change all <link> related to css to <asset:link>
  • Change all path in templates from ../ to /
<g:render template="../layouts/bottomBar"/>
to
<g:render template="/layouts/bottomBar"/>
  • Remove all grailsApplication beans from Controllers as Grails 3 imports them automatically
def grailsApplication
  • Install new plugin called ajax-tags as Grails 3 removes all taglibs related to remote and to keep using them we will need to have it installed
// Ajax Tags plugin
compile "org.grails.plugins:ajax-tags:1.0.1.BUILD-SNAPSHOT"
  • Change all prefixes about respond method convention
ExampleArgument TypeCalculated Model Variable
respond Book.list()java.util.ListbookList
respond Book.get(1)example.Bookbook
respond( [1,2] )java.util.ListintegerList
respond( [1,2] as Set )java.util.SetintegerSet
respond( [1,2] as Integer[] )Integer[]integerArray

// course/_grid
<g:each in="${courseInstanceList}" status="i" var="courseInstance">
to
<g:each in="${courseList}" status="i" var="courseInstance">
  • Change all references to config
grailsApplication.config.net.rdcstudios.pagination.size
to
grailsApplication.config.getProperty('net.rdcstudios.pagination.size')
  • Change all references to grailsResourceLocator
grailsResourceLocator.findResourceForURI()
to
assetResourceLocator.findAssetForURI()
  • Change all references to domainClass
instance.domainClass.propertyName
to
grails.util.GrailsNameUtils.getPropertyName(instance.class)
  • To make external config properties work, we will need to modify the class Application.groovy
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import org.springframework.context.EnvironmentAware
import org.springframework.core.env.Environment
import org.springframework.core.env.MapPropertySource

class Application extends GrailsAutoConfiguration implements EnvironmentAware {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }

    @Override
    void setEnvironment(Environment environment) {
        def file = new File("/etc/core/core-${grails.util.Environment.getCurrent().name}-config.properties")
        if(file.exists()) {
            def config = new ConfigSlurper().parse(file.text)
            environment.propertySources.addFirst(new MapPropertySource(grails.util.Environment.getCurrent().name, config))
        }
    }
}
  • Plus to escape all values from external config properties we will need to add quotes "..." as YAML Parser launches :, . and / values as yml values
dataSource.url = jdbc:mysql://localhost/rdcstudios_dev
to
dataSource.url = "jdbc:mysql://localhost/rdcstudios_dev"
  • Move all global constraints from Config.groovy to application.groovy
// Shared constraints
grails.gorm.default.constraints = {
    codeConstraint(blank: false, nullable: true, size: 1..4, unique: 'country')
}
  • Don't forget to change empty constraints from () to [:]
static constraints = {
  name nullable: true, blank: false
  mySimpleProperty()                  // <- A field that has no constraints. This syntax is not supported in Grails 3.
  anotherProperty unique: true
}
to
static constraints = {
  name nullable: true, blank: false
  mySimpleProperty [:]                // <- Empty map argument instead of ()
  anotherProperty unique: true
}
  • All services injected into domain classes don't work. Why? In Grails 2 projects they were working property. The reason is that in Grails 3 there is a property called grails.gorm.autowired in application.yml set to false by default. This property is disabled by default for performance reasons.
Further info
http://stackoverflow.com/questions/43059313/grails-3-2-8-dependency-injection-in-domain-classes
https://gist.github.com/erichelgeson/be2f9f62ab63d989f2ec962ae7001f21
  • Every time app throws an exception is displayed the following:
Caused by: groovy.lang.MissingMethodException: No signature of method: ch.qos.logback.classic.Logger.error() is applicable for argument types: (org.springframework.dao.DataIntegrityViolationException)

Why? This is because the default logger that is added to Grails artefacts is now an instance of SLF4J, and not JCL. Unlike JCL, none of the SLF4J logging methods take a single Object argument. Therefore we need to do some changes:

catch (DataIntegrityViolationException dive) {
    log.error(dive)
    flash.error = message(code: 'default.not.deleted.message', args: ["User", id], default: "User could not be deleted")
    redirect(action: 'show', id: userInstance?.id)
}

to

catch (DataIntegrityViolationException dive) {
    log.error(dive.toString())
    flash.error = message(code: 'default.not.deleted.message', args: ["User", id], default: "User could not be deleted")
    redirect(action: 'show', id: userInstance?.id)
}

Further info
https://github.com/grails/grails-core/issues/9971
http://docs.grails.org/3.2.8/guide/single.html#upgrading31x (See Slf4j now default section)

UNIT TESTS

No changes

INTEGRATION TESTS

  • I changed extends IntegrationSpec to Specification from all tests to keep rules as per Grails 3 doc.
  • I added @Integration and @Rollback annotation at class level.
    @Integration
    @Rollback
    class ActivityServiceIntegrationSpec extends Specification {
    ...
    }
  • I had to change some load methods to get method as in Grails 3 data binding is more stricted.
def setupData() {
    def user = User.load(1)
    def course = new Course(shortName: 'Test shortname', summary: 'Test summary', owner: user).save() -> LazyInitializationException
}

to 

def setupData() {
    def user = User.get(1)
    def course = new Course(shortName: 'Test shortname', summary: 'Test summary', owner: user).save() -> Works fine
}
  • As per Grails 3 it's not allowed to save any instance in setup method when @Rollback annotation is not added as app will throw HibernateException: No Session found. To solve this it's recommended to added @Rollback annotation and separate setup method (for non transactional operations) from setupData method (for transactional operations). Don't forget calling setupData method from given block
def setup() {
    alfrescoService.metaClass.uploadFile = { Object resource, MultipartFile resourceFile -> "1" }
    alfrescoService.metaClass.deleteFileByNodeRef = { String nodeRef -> true }
}

def setupData() {
    def user = User.get(1)
    def course = new Course(shortName: 'Test shortname', summary: 'Test summary', owner: user).save()
}

...

void "test save method"() {
    given:
    setupData()
...
}

You have two possibilities:
  • Adding @Rollback you will have Hibernate session in setup method but once finished test there will be no rollback. If you split it in setup method for non transactional operations and setupData method for transactional operations, you will get rollback.
  • Without adding @Rollback you will get no Hibernate session in setup method.

DEPLOYMENT ON JENKINS

  • Below I'll paste changes I had to do in order to migrate deployment to Grails 3 on Jenkins job

Grails 2 jobGrails 3 job
GENERATE WAR FILEgrails war education.wargrails war
RUN TESTSgrails test-app -coverage -xmlgrails test-app
GENERATE DOCgrails docgrails docs



PUBLISH COVERAGE REPORTStarget/test-reports/cobertura/coverage.xmlbuild/reports/cobertura/coverage.xml
DEPLOY TO TOMCAT(no changes)(no changes)
EMAIL(no changes)(no changes)
  • On Grails 2 war file was generated under project root path but on Grails 3 war file is generated in build / libs path so I had to add some lines to change path in build.gradle
// build.gradle file
war {
    archiveName "${baseName}.war" // This line for not adding version to war file name
    destinationDir project.projectDir // This line for setting path to project root path and not in build / libs
}
  • When deploying to Tomcat Jenkins threw this weird error
java.lang.NoSuchMethodError: org.apache.tomcat.util.res.StringManager.getManager(Ljava/lang/Class;)Lorg/apache/tomcat/util/res/StringManager;

After googling a little bit, I figured out it was due to use different Tomcat versions among embed Tomcat used by Spring Boot (version 8) and Tomcat used by Jenkins (version 7). As a workaround I changed this

// build.gradle file
compile "org.springframework.boot:spring-boot-starter-tomcat"
to
provided "org.springframework.boot:spring-boot-starter-tomcat"

Further info -> http://www.tothenew.com/blog/grails-3-and-deployment-to-tomcat-container/
  • To keep using coverage plugin as of now you can use Gradle plugin. For it the only thing you need to do is to add some lines to build.gradle file
buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "net.saliman:gradle-cobertura-plugin:2.4.0"
  }
}

apply plugin: "net.saliman.cobertura"

Further info -> https://plugins.gradle.org/plugin/net.saliman.cobertura
  • Also you will need to add cobertura after running tests
// Cobertura configuration
cobertura {
    coverageFormats = ['xml']

    coverageExcludes = ['.*Application.*',
                        '.*BootStrap.*',
                        '.*UrlMappings.*']
}
test.finalizedBy(project.tasks.cobertura)

INTELLIJ IDEA 2017

  • Execute all tests under JUnit not under Grails environment, otherwise Intellij IDEA will execute all tests.

  • Sometimes Intellij IDEA executes the test under production or development env instead of test env. To amend it you just have to add -Dgrails.env=production to command line from Run / Debug configuration
// Test log trace
The following profiles are active: production
  • Sometimes test throws an error related to Cannot add Domain Class [class ...] It is not a Domain!. To fix it,
grails clean

SPRING SECURITY CORE

  • Our Config.groovy in Grails 2 project contained all configuration properties related to plugin. As of now we will have to create a new file called application.groovy to put them in
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.debug.useFilter = true
grails.plugin.springsecurity.securityConfigType = "Annotation"
grails.plugin.springsecurity.logout.postOnly = false
grails.plugin.springsecurity.userLookup.userDomainClassName = "net.rdcstudios.core.security.User"
grails.plugin.springsecurity.userLookup.authorityJoinClassName = "net.rdcstudios.core.security.UserRole"
grails.plugin.springsecurity.authority.className = "net.rdcstudios.core.security.Role"
grails.plugin.springsecurity.adh.errorPage = null
grails.plugin.springsecurity.rejectIfNoRule = false
grails.plugin.springsecurity.fii.rejectPublicInvocations = false
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
        [pattern: '/',                              access: ['permitAll']],
        [pattern: '/index',                         access: ['permitAll']],
        [pattern: '/index.gsp',                     access: ['permitAll']],
        [pattern: '/assets/**',                     access: ['permitAll']]
]

grails.plugin.springsecurity.useSecurityEventListener = true
  • Plus as we need to load an event related to plugin at runtime, we will create other file called runtime.groovy to define it
grails.plugin.springsecurity.onInteractiveAuthenticationSuccessEvent = { e, appCtx ->
    // Setting language by country and language fields from user domain class
    net.rdcstudios.core.security.User.withSession {
        net.rdcstudios.core.security.User user = net.rdcstudios.core.security.User.get(e.source.principal.id)
        def languageCode = user?.language?.code
        def countryCode = user?.country?.code
        def request = grails.plugin.springsecurity.web.SecurityRequestHolder.getRequest()
        if (languageCode && countryCode && request) {
            def locale = new Locale(languageCode, countryCode)
            if (locale) {
                org.grails.web.util.WebUtils.setSessionAttribute(request, org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale)
            }
        }
    }
}

DATABASE MIGRATION

  • We could not install the new plugin for Grails 3 as we were working with Java 7 and app threw this error,
Unsupported major.minor version 52.0

INFO
J2SE 8 = 52
J2SE 7 = 51 
J2SE 6.0 = 50  
J2SE 5.0 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45
When upgrading to Java 8 error disappeared.

EXPORT

  • I found an error to display export buttons. In Grails 2 projects you set it by adding Resource plugin tag
<r:require module="export"/>
 
But in Grails 3 projects we use Asset Pipeline so no way to reference resources belonging to plugins. I realized manifest file from Export plugin was called application.css (the same name as my app one). So as a workaround I had to rename my app manifest to app.css and leave application.css for Export plugin one.

<asset:stylesheet src="application.css"/>
 
I had to add this line just on views where it was used.
  • Plus I had to add the following line to build.gradle
assets {
    minifyJs = true
    minifyCss = true
    packagePlugin = true 
}

ASSET PIPELINE DOCUMENTATION
Plugins also can have the same "grails-app/assets" folder and their URL mapping is also the same. This means it can be more important to ensure unique naming / path mapping between plugins. This is also powerful in the sense that a plugin can add helper manifests to be used within your apps like jquery, bootstrap, font-awesome, and more.

Plugins should make sure that the assets { packagePlugin } property is set to true in their build.gradle file otherwise assets will not properly be packaged into the plugin for use by the application.

JASPER

  • I found no plugin compatible for Grails 3 so I had to do the following:
    1. Import JasperController to my controllers folder
    2. Import JasperService to my services folder
    3. Import JasperTagLib to my taglib folder (changing all g.resource references to asset.assetPath)
    4. Import all Jasper images to my assets/images folder
    5. Import JasperExportFormat and JasperReportDef to src/main/groovy
  • Plus I had to add the following lines to build.gradle
// Jasper
compile "com.lowagie:itext:2.1.7"

compile("net.sf.jasperreports:jasperreports:5.6.1") {
    exclude group: 'antlr', module: 'antlr'
    exclude group: 'commons-logging', module: 'commons-logging'
    exclude group: 'org.apache.ant', module: 'ant'
    exclude group: 'mondrian', module: 'mondrian'
    exclude group: 'commons-javaflow', module: 'commons-javaflow'
    exclude group: 'net.sourceforge.barbecue', module: 'barbecue'
    exclude group: 'xml-apis', module: 'xml-apis'
    exclude group: 'xalan', module: 'xalan'
    exclude group: 'org.codehaus.groovy', module: 'groovy-all'
    exclude group: 'org.hibernate ', module: 'hibernate'
    exclude group: 'javax.xml.soap', module: 'saaj-api'
    exclude group: 'javax.servlet', module: 'servlet-api'
    exclude group: 'org.springframework', module: 'spring-core'
    exclude group: 'org.beanshell', module: 'bsh'
    exclude group: 'org.springframework', module: 'spring-beans'
    exclude group: 'jaxen', module: 'jaxen'
    exclude group: 'net.sf.barcode4j ', module: 'barcode4j'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-svg-dom'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-xml'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-awt-util'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-dom'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-css'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-gvt'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-script'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-svggen'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-util'
    exclude group: 'org.apache.xmlgraphics', module: 'batik-bridge'
    exclude group: 'javax.persistence', module: 'persistence-api'
    exclude group: 'eclipse', module: 'jdtcore'
    exclude group: 'org.olap4j', module: 'olap4j'
}

compile "org.apache.poi:poi:3.10-FINAL"
compile "commons-io:commons-io:2.5"

BULK DATA IMPORTS

  • I found no plugin compatible for Grails 3 so I had to do the following:
    1. Import ImportsController to my controllers folder
    2. Import ImportsService to my services folder
    3. Import DefaultImporter and ImportsException to src/main/groovy. Plus logging folder (with all Logguers) to src/main/groovy too.
  • Plus I had to copy all code from doWithSpring closure of BulkDataImportsGrailsPlugin in Grails 2 to doWithSpring closure of Application in Grails 3
class Application extends GrailsAutoConfiguration implements EnvironmentAware {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }

    @Override
    void setEnvironment(Environment environment) {
        def file = new File("/etc/core/core-${grails.util.Environment.getCurrent().name}-config.properties")
        if(file.exists()) {
            def config = new ConfigSlurper().parse(file.text)
            environment.propertySources.addFirst(new MapPropertySource(grails.util.Environment.getCurrent().name, config))
        }
    }

    @Override
    Closure doWithSpring() {
        def loggingProvider = Holders.config.grails.plugins.imports.containsKey('loggingProvider') ? Holders.config.grails.plugins.imports.loggingProvider : 'default'

        def loggingProviderClass = null
        if (loggingProvider == 'mongo') {
            loggingProviderClass = MongoLogger
        } else if (loggingProvider == 'mem') {
            loggingProviderClass = InMemoryLogger
        } else if (loggingProvider == 'default') {
            loggingProviderClass = DefaultLogger
        } else {
            Class clazz = Class.forName(loggingProvider, true, Thread.currentThread().contextClassLoader)
            loggingProviderClass = clazz
        }

        def beans = {
            importsLogger(loggingProviderClass)
        }

        for(service in Holders.grailsApplication.serviceClasses) {
            if (service.hasProperty('imports')) {
                def entityName,
                    imports = service.getPropertyValue('imports')
                if (imports instanceof Class || imports instanceof String) {
                    entityName = GrailsNameUtils.getPropertyName(imports)
                }
                if (entityName) {
                    def found = Holders.grailsApplication.domainClasses?.find { GrailsNameUtils.getPropertyName(it.name) == entityName} != null
                    if (!found  && !service.hasMetaMethod('processRow', getArgs(5)) ) {
                        log.warn('\n    BulkDataImports: could not configure importer '+service.shortName+'... no domain class found and missing processRow method')
                    } else {
                        DefaultImporter.SERVICE_METHODS.each { k, v->
                            if (!service.hasMetaMethod(k, getArgs(v))) service.metaClass."${k}" = DefaultImporter."${k}"
                        }
                        def props = DefaultImporter.SERVICE_PROPERTIES.clone()
                        props.entityName = entityName
                        props.each {k, v->
                            if (!service.hasMetaMethod(k)) service.metaClass."${k}" = { ->  v }
                        }
                        ImporterService.IMPORT_CONFIGURATIONS[entityName] = GrailsNameUtils.getPropertyNameRepresentation(service.shortName)
                    }
                } else {
                    log.warn('\n    BulkDataImports: invalid imports configuration for '+service.shortName+' :'+imports)
                }
            }

        }

        ImporterService.IMPORT_CONFIGURATIONS.each { k, v-> log.info('\n    BulkDataImports:'+ k + ' imported by '+v) }

        return beans
    }

    private getArgs(ct) {
        (1..ct).collect { Object.class }.toArray()
    }
}

Hope this helps.

3 comentarios:

  1. Thank you so much for sharing detailed process of migrating grails 2.x to grails 3.x!

    As a side note , your English is very good !

    ResponderEliminar
  2. thanks now I use Jasper in Grails 3 because does not exist the definitive guide to use Birt with Grails 3.
    Alvaro León Silvano

    ResponderEliminar
  3. Wow, this is amazing! You have done a great service here. I am close to completing my Grails 2x->3.3.8 upgrade and this has been very helpful. And you got Jasper to work?? Amazing.
    I hope I can contribute some more at some point as I am scripting a lot of the file copy/edit operations. It's very time consuming to make this conversion while the source system is under constant revision.
    Thanks again

    ResponderEliminar