April 28, 2011

Groovy Goodness: Recursion with Closure Trampoline Capability

When we write recursive code we might get a stack overflow exception, because calls are placed on the stack to be resolved. Since Groovy 1.8 we can use the trampoline capability of closures to overcome this problem.

We invoke a trampoline() method on a closure and our original closure is now wrapped in TrampolineClosure instance. Calls to the TrampolineClosure are executed sequentially invoking the original closure, until the original closure returns something else then a TrampolineClosure instance. This way the stack isn't filled and we won't get the stack overflow exceptions.

def sizeList
sizeList = { list, counter = 0 ->
    if (list.size() == 0) {
        counter
    } else {
        sizeList.trampoline(list.tail(), counter + 1)
    }
}
sizeList = sizeList.trampoline()

assert sizeList(1..10000) == 10000

Try with Groovy web console.

April 27, 2011

Groovy Goodness: New Dollar Slashy Strings

Groovy already has a lot of ways to define a String value, and with Groovy 1.8 we have another one: the dollar slashy String. This is closely related to the slashy String definition we already knew (which also can be multi-line by the way, added in Groovy 1.8), but with different escaping rules. We don't have to escape a slash if we use the dollar slashy String format, which we would have to do otherwise.

def source = 'Read more about "Groovy" at http://mrhaki.blogspot.com/'

// 'Normal' slashy String, we need to escape / with \/
def regexp = /.*"(.*)".*\/(.*)\//  

def matcher = source =~ regexp
assert matcher[0][1] == 'Groovy'
assert matcher[0][2] == 'mrhaki.blogspot.com'

// Dollar slash String.
def regexpDollar = $/.*"(.*)".*/(.*)//$  

def matcherDollar = source =~ regexpDollar
assert matcherDollar[0][1] == 'Groovy'
assert matcherDollar[0][2] == 'mrhaki.blogspot.com'

def multiline = $/
Also multilines 
are supported.
/$

Groovy Goodness: Chain Closures Together with Closure Composition

There are a lot of new features in Groovy 1.8. One of them is the possibility to compose a new closure by chaining two other closures together. We use the leftShift and rightShift operators (<< and >>) to combine multiple closures to create a new closure.

def convert = { new Expando(language: it) }
def upper = { it.toUpperCase() }

// Composition.
def upperConvert = convert << upper

def languages = ['Groovy', 'Scala', 'Clojure'].collect(upperConvert)
println languages // Output: [{language=GROOVY}, {language=SCALA}, {language=CLOJURE}]
assert languages[0].language == 'GROOVY'
assert languages[1].language == 'SCALA'
assert languages[2].language == 'CLOJURE'

// Reverse composition.
def lastLetter = { it[-1] }
def firstLetters = ['Groovy', 'Clojure', 'Scala'].collect(upper >> lastLetter)
assert firstLetters.join() == 'YEA'

Try with Groovy web console.

Groovy Goodness: See if Sets are Equal

Since Groovy 1.8 we can use the equals() method to compare the contents of two Set collections. The two sets are equals if they have the same size and all elements are in both sets.

def set1 = ['Java', 42, true] as Set
def set2 = ['Groovy', 42, true, 'Java'] as Set
def set3 = [42L, true, 'Java'] as Set

assert set1.equals(set3)
assert set1 == set3
assert !set1.equals(set2)
assert set2 != set3

Groovy Goodness: Add Items to a List at Specified Position

Since Groovy 1.8 we can create a new list from two lists, where the second list is 'inserted' at a specified index position in the first list. The original lists are not changed: the result is a new list. Groovy also adds the addAll() method to List objects (since Groovy 1.7.2), but then the original list object is changed.

def list = ['Gr', 'vy']

assert list.plus(1, 'oo') == ['Gr', 'oo', 'vy']
assert list == ['Gr', 'vy']

list.addAll(1, 'oo') 
assert list == ['Gr', 'oo', 'vy']

assert (1..10).plus(5, 6..7) == [1,2,3,4,5,6,7,6,7,8,9,10]

Groovy Goodness: Convert Collection to Set with Only Unique Elements

Since Groovy 1.8 we can convert an array or collection to a Set with only the unique elements of the original array or collection. We use the toSet() method to do this.

def numbers = [1,2,1,4,1,2] as int[]
assert numbers.toSet() == [1,2,4] as Set

def list = ['Groovy', 'Java', 'Grails', 'Groovy']
assert list.toSet() == ['Groovy', 'Java', 'Grails'] as Set

Groovy Goodness: Get Unique Characters in a String

Groovy adds the toSet() method to the String class in version 1.8. With this method we get a Set of unique String values from the original String value.

String s = 'Groovy is gr8!'

assert s.toSet().sort().join() == ' !8Ggiorsvy'

Groovy Goodness: Generate equals and hashcode Methods with EqualsAndHashCode Annotation

There are a lot of new bytecode generation annotation in Groovy 1.8. One of them is the @EqualsAndHashCode annotation. With this annotation an equals() and hashCode() method is generated for a class. The hashCode() method is implemented using Groovy's org.codehaus.groovy.util.HashCodeHelper (following an algorithm from the book Effective Java). The equals() method looks at all the single properties of a class to see of both objects are the same.

We can even include class fields instead of only properties for generating both methods. We only have to use includeFields=true when we assign the annotation.

To include calls to a super class we use the annotation attribute callSuper and assign the value true. Finally we can also exclude properties or fields from hashcode calculation or equal comparisons. We use the annotation attribute excludes for this and we can assign a list of property and field names.

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode(includeFields=true)
class User {
    String name
    boolean active
    List likes
    private int age = 37
}

def user = new User(name: 'mrhaki', active: false, likes: ['Groovy', 'Java'])
def mrhaki = new User(name: 'mrhaki', likes: ['Groovy', 'Java'])
def hubert = new User(name: 'Hubert Klein Ikkink', likes: ['Groovy', 'Java'])

assert user == mrhaki
assert mrhaki != hubert

Set users = new HashSet()
users.add user
users.add mrhaki
users.add hubert
assert users.size() == 2

Groovy Goodness: Tuple Constructor Creation

Groovy 1.8 adds the @TupleConstructor annotation. With this annotation we can automatically create a tuple constructor at compile time. So the constructor can be found in the compiled class. For each property in the class a parameter in the constructor is created with a default value. The order of the properties defined in the class also defines the order of parameters in the constructor. Because the parameters have default values we can use Groovy syntax and leave parameters at the end of the parameter list out when we use the constructor.

We can also include fields as constructor parameters. We use the annotation attribute includeFields=true to activate this.

If we define our constructors in the class than the TupleConstructor annotation will not create extra constructors. But we can override this behaviour with the attribute value force=true. We have to make sure we don't have constructor conflicts ourselves, because now the annotation will create the extra constructors.

If our class extends another class and we want to include the properties or fields of the super class we can use the attributes includeSuperProperties and includeSuperFields. We can even instruct the annotation to create code in the constructor to call the super constructor of the super class with the properties. We must set the annotation attribute callSuper=true to make this happen.

import groovy.transform.TupleConstructor

@TupleConstructor()
class Person {
    String name
    List likes
    private boolean active = false
}

def person = new Person('mrhaki', ['Groovy', 'Java'])

assert person.name == 'mrhaki'
assert person.likes == ['Groovy', 'Java']

person = new Person('mrhaki')

assert person.name == 'mrhaki'
assert !person.likes
// includeFields in the constructor creation.
import groovy.transform.TupleConstructor

@TupleConstructor(includeFields=true)
class Person {
    String name
    List likes
    private boolean active = false

    boolean isActivated() { active }
}

def person = new Person('mrhaki', ['Groovy', 'Java'], true)

assert person.name == 'mrhaki'
assert person.likes == ['Groovy', 'Java']
assert person.activated
// use force attribute to force creation of constructor
// even if we define our own constructors.
import groovy.transform.TupleConstructor

@TupleConstructor(force=true)
class Person {
    String name
    List likes
    private boolean active = false

    Person(boolean active) {
        this.active = active
    }

    boolean isActivated() { active }
}

def person = new Person('mrhaki', ['Groovy', 'Java'])

assert person.name == 'mrhaki'
assert person.likes == ['Groovy', 'Java']
assert !person.activated

person = new Person(true)

assert person.activated
// include properties and fields from super class.
import groovy.transform.TupleConstructor

@TupleConstructor(includeFields=true)
class Person {
    String name
    List likes
    private boolean active = false

    boolean isActivated() { active }
}

@TupleConstructor(callSuper=true, includeSuperProperties=true, includeSuperFields=true)
class Student extends Person {
    List courses
}

def student = new Student('mrhaki', ['Groovy', 'Java'], true, ['IT'])

assert student.name == 'mrhaki'
assert student.likes == ['Groovy', 'Java']
assert student.activated
assert student.courses == ['IT']

Groovy Goodness: Change Scope Script Variable with Field Annotation

The scope of a script variable changes once we add a type definition to the variable. Then the variable is no longer visible in methods of the script. To make a type variable visible in the entire script scope we add the annotation @Field. This way the variable has a type and can be used in methods of the script.

import groovy.transform.Field

String stringValue = 'I am typed without @Field.'
def i = 42
@Field String stringField = 'I am typed with @Field.'
counter = 0 // No explicit type definition.

def runIt() {
    try {
        assert stringValue == 'I am typed without @Field.'
    } catch (e) {
        assert e instanceof MissingPropertyException
    }
    try {
        assert i == 42
    } catch (e) {
        assert e instanceof MissingPropertyException
    }
    
    assert stringField == 'I am typed with @Field.'
    
    assert counter++ == 0
}

runIt()

assert stringValue == 'I am typed without @Field.'
assert stringField == 'I am typed with @Field.'
assert i == 42
assert counter == 1

Groovy Goodness: Group and Count Elements in Collection or Map

Since Groovy 1.8 we can group items in a collection, map or array and count the number of elements in a group. We use a closure to define the keys for the grouping. Then the number of elements in the group are counted and that is the value of the grouping key.

def list = ['Groovy', 'Grails', 'Java']
assert list.countBy { it[0] } == ['G': 2, 'J': 1]  // 2 items start with G, 1 with J.

def numbers = [1,2,3,4,5] as Integer[]
assert numbers.countBy { it % 2 } == [0: 2, 1: 3]  // 2 even, 3 uneven numbers

def map = [user: 'mrhaki', city: 'Tilburg', age: 37]
assert map.countBy { key, value -> value.class } == [(String.class): 2, (Integer.class): 1]  // 2 values of type String and 1 of type Integer

Groovy Goodness: Count Occurrences in a Collection or Map

We can count occurrences of elements in a collection that apply to a condition given by a closure. The closure contains the condition the elements we want to count must apply to. We can use the new count() method since Groovy 1.8 and it can be applied to collections, maps and arrays.

def list = ['Groovy', 'Grails', 'Java']
assert list.count { it.startsWith('G') } == 2

def numbers = [1,2,3,4] as Integer[]
assert numbers.count { it > 2 } == 2

def map = [user: 'mrhaki', city: 'Tilburg', age: 37]
assert map.count { key, value -> key.size() == 3 } == 1

Groovy Goodness: Easy toString Creation for Our Classes

Since Groovy 1.8 we can use the @ToString annotation for easy creation of a toString() method. We only have to add the annotation to our class definition and we get a nicely formatted output of the properties of our class.

We can even customize what we want to see in the output. We can see the names of the properties of our class in the toString() output if we add the attribute includeNames=true. By default only properties are added to the output, but we can include fields as well with the annotation attribute includeFields=true. To exclude properties we use the attribute excludes and assign the names of the properties we don't want in the output separated by a comma.

Finally we can include properties from a super class with the annotation atribute includeSuper=true.

Let's see the @ToString in action with a few samples:

// Most simple implementation of toString.
import groovy.transform.ToString

@ToString
class Person {
    String name
    List likes
    private boolean active = false
}

def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java'])

assert person.toString() == 'Person(mrhaki, [Groovy, Java])'
// includeNames to output the names of the properties.
import groovy.transform.ToString

@ToString(includeNames=true)
class Person {
    String name
    List likes
    private boolean active = false
}

def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java'])

assert person.toString() == 'Person(name:mrhaki, likes:[Groovy, Java])'
// includeFields to not only output properties, but also field values.
import groovy.transform.ToString

@ToString(includeNames=true, includeFields=true)
class Person {
    String name
    List likes
    private boolean active = false
}

def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java'])

assert person.toString() == 'Person(name:mrhaki, likes:[Groovy, Java], active:false)'
// Use includeSuper to include properties from super class in output.
import groovy.transform.ToString

@ToString(includeNames=true)
class Person {
    String name
    List likes
    private boolean active = false
}

@ToString(includeSuper=true, includeNames=true)
class Student extends Person {
    List courses
}

def student = new Student(name: 'mrhaki', likes: ['Groovy', 'Java'], courses: ['IT', 'Business'])

assert student.toString() == 'Student(courses:[IT, Business], super:Person(name:mrhaki, likes:[Groovy, Java]))'
// excludes active field and likes property from output
import groovy.transform.ToString

@ToString(includeNames=true, includeFields=true, excludes='active,likes')
class Person {
    String name
    List likes
    private boolean active = false
}

def person = new Person(name: 'mrhaki', likes: ['Groovy', 'Java'])

assert person.toString() == 'Person(name:mrhaki)'

Groovy Goodness: Check if Maps are Equal

With Groovy 1.8 the equals() method is added to Map. This means we can check if maps are equals. They are equals if both maps have the same size, and keys and values are the same.

def map1 = [user: 'mrhaki', likes: 'Groovy', age: 37]
def map2 = [age: 37.0, likes: 'Groovy', user: 'mrhaki']
def map3 = [user: 'Hubert Klein Ikkink', likes: 'Groovy']

assert map1.equals(map2)
assert map1 == map2
assert !map1.equals(map3)
assert map2 != map3

Groovy Goodness: Inject Logging Using Annotations

With Groovy 1.8 we can inject a log field into our classes with a simple annotation. In our class we can invoke method on the log field, just as we would do if we wrote the code to inject the log field ourselves. How many times have we written code like this Logger log = LoggerFactory.getLogger(<class>) at the top of our classes to use for example the Slf4j API? Since Groovy 1.8 we only have to add the @Slf4j annotation to our class and get the same result. AND each invocation of a log method is encapsulated in a check to see if the log level is enabled.

// File: LogSlf4j.groovy
// Add dependencies for Slf4j API and Logback
@Grapes([
    @Grab(group='org.slf4j', module='slf4j-api', version='1.6.1'),
    @Grab(group='ch.qos.logback', module='logback-classic', version='0.9.28')
])
import org.slf4j.*
import groovy.util.logging.Slf4j

// Use annotation to inject log field into the class.
@Slf4j
class HelloWorldSlf4j {
    def execute() {
        log.debug 'Execute HelloWorld.'
        log.info 'Simple sample to show log field is injected.'
    }
}

def helloWorld = new HelloWorldSlf4j()
helloWorld.execute()

When we run this script we get the following output:

$ groovy LogSlf4j.groovy
21:20:02.392 [main] DEBUG HelloWorldSlf4j - Execute HelloWorld.
21:20:02.398 [main] INFO  HelloWorldSlf4j - Simple sample to show log field is injected.

Besides an annotation for the Slf4j API other logging frameworks are supported with annotations:

Logging frameworkAnnotation
java.util.logging@Log
Log4j@Log4j
Apache Commons Logging@Commons
Slf4j API@Slf4j

Groovy Goodness: Build JSON with JsonBuilder and Pretty Print JSON Text

Groovy 1.8 adds JSON support. We can build a JSON data structure with the JsonBuilder class. This class functions just like other builder classes. We define a hiearchy with values and this is converted to JSON output when we view the String value. We notice the syntax is the same as for a MarkupBuilder.

import groovy.json.*

def json = new JsonBuilder()

json.message {
    header {
        from('mrhaki')  // parenthesis are optional
        to 'Groovy Users', 'Java Users'
    }
    body "Check out Groovy's gr8 JSON support."
}

assert json.toString() == '{"message":{"header":{"from":"mrhaki","to":["Groovy Users","Java Users"]},"body":"Check out Groovy\'s gr8 JSON support."}}'

// We can even pretty print the JSON output
def prettyJson = JsonOutput.prettyPrint(json.toString())
assert prettyJson == '''{
    "message": {
        "header": {
            "from": "mrhaki",
            "to": [
                "Groovy Users",
                "Java Users"
            ]
        },
        "body": "Check out Groovy's gr8 JSON support."
    }
}'''

Groovy Goodness: Parse JSON with JsonSlurper

With Groovy 1.8 we can parse JSON text with the JsonSlurper class. We only have to feed the text to the parseText() method and we can the values are mapped to Maps and Lists. And getting the content then is very easy:

import groovy.json.*

def jsonText = '''
{
    "message": {
        "header": {
            "from": "mrhaki",
            "to": ["Groovy Users", "Java Users"]
        },
        "body": "Check out Groovy's gr8 JSON support."
    }
}       
'''

def json = new JsonSlurper().parseText(jsonText)

def header = json.message.header
assert header.from == 'mrhaki'
assert header.to[0] == 'Groovy Users'
assert header.to[1] == 'Java Users'
assert json.message.body == "Check out Groovy's gr8 JSON support."

April 13, 2011

Grails Goodness: Applying Layouts in Layouts Revisited

In the blog post Applying Layouts in Layouts we learned how to reuse layouts in a Grails application. Peter Ledbrook added a comment suggesting to use the <g:applyLayout/> in the Groovy Server Pages in the layouts directory instead of in the views. So this way we can use the default method of applying layouts in our views (using for example the <meta/> tag) and keep all <g:applyLayout/> tags in the layout pages. In this post we see how we can support this for our applications and still reuse the layouts like before.

Please read the previous blog post to see the layout structure of our sample Grails application. The main layout page is still grails-app/views/layouts/page.gsp and doesn't need to be changed:

<%-- File: grails-app/views/layouts/page.gsp --%>
<!DOCTYPE html>
<html>
<head>
    <title><g:layoutTitle default="Grails"/></title>
    <link rel="stylesheet" href="${resource(dir: 'css', file: 'main.css')}"/>
    <link rel="stylesheet" href="${resource(dir: 'css', file: 'layout.css')}"/>
    <link rel="stylesheet" href="${resource(dir: 'css', file: 'fonts.css')}"/>
    <link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon"/>
    <g:layoutHead/>
</head>
<body>
<div id="header" class="clearfix">
    <div id="logo">
        <g:link uri="/"><g:message code="nav.home"/></g:link>
        <p><g:message code="title.website"/></p>
    </div>

    <div id="searchform">
        <g:form controller="search">
            <fieldset class="search">
                <input type="text" class="search-input" value="${message(code:'search.box.search')}" name="search" id="search-phrase" maxlength="100"/>
                <input type="submit" value="${message(code: 'search.box.submit')}" />
            </fieldset>
        </g:form>
    </div>

    <div id="navmenu">
        <ul>
            <li><g:link uri="/"><g:message code="nav.home"/></g:link></li>
            <li><g:link controller="product" action="list"><g:message code="nav.products"/></g:link></li>
        </ul>
    </div>
</div>

<g:layoutBody/>

<div id="footer">
    <p>Copyright © 2011 Hubert A. Klein Ikkink - <a href="http://www.mrhaki.com">mrhaki</a></p>
</div>
</body>
</html>

The layout for the homepage is defined by fiveblocks.gsp in the layouts directory. We change this file and use <g:applyLayout/> to apply the main page layout instead of using <meta name="layout" content="page"/>:

<%-- File: grails-app/views/layouts/fiveblocks.gsp --%>
<g:applyLayout name="page">
    <html>
    <head>
        <title><g:layoutTitle/></title>
        <g:layoutHead/>
    </head>
    <body>
    <div id="banner">
        <g:pageProperty name="page.banner"/>
    </div>

    <div id="left">
        <g:pageProperty name="page.left1"/>
        <g:pageProperty name="page.left2"/>
        <g:pageProperty name="page.left3"/>

        <div id="box-left">
            <g:pageProperty name="page.box-left"/>
        </div>

        <div id="box-right">
            <g:pageProperty name="page.box-right"/>
        </div>
    </div>

    <div id="right">
        <g:pageProperty name="page.right1"/>
        <g:pageProperty name="page.right2"/>
        <g:pageProperty name="page.right3"/>
    </div>
    </body>
    </html>
</g:applyLayout>

And now we use <meta name="layout" content="fiveblocks"/> in the homepage Groovy server page and define the content for our Sitemesh content blocks.

<%-- File: grails-app/views/templates/homepage.gsp --%>
<html>
<head>
    <meta name="layout" content="fiveblocks"/>
    <title><g:message code="title.homepage"/></title>
</head>
<body>
<content tag="banner">
    <h1>Welcome to Grails Layout Demo</h1>
</content>

<content tag="left1">
    <p>...</p>
</content>

<content tag="box-left">
    <p>...</p>
</content>

<content tag="box-right">
    <p>...</p>
</content>

<content tag="right1"> 
    <p>...</p> 
</content> 
</body> 
</html> 

We notice the changes aren't big compared to the previous solution, but using <meta name="layout" content="..."/> in the views is more compliant to what we already learned when using layouts in views.

Also keep in mind Peter Ledbrook's warning about having layouts nested more than one deep:

Finally, if you do have layouts nested more than one deep (not necessarily a good idea, but that's what grails.org is currently doing) then it's worth noting that every layout must have the <g:layout*/> and <g:pageProperty/> tags, otherwise the contents don't propagate properly.

Sources of the sample Grails application can be found at GitHub.

April 5, 2011

Grails Goodness: Add Additional Web Application to Tomcat

In this post we learn how to add an additional WAR file to the Tomcat context when we use $ grails run-app. Recently I was working on a Alfresco Web Quick Start integration with Grails (current progress on GitHub). Alfresco offers inline editing of the web content with the help of an extra Java web application. This web application needs to be accessible from the Grails application, so at development time I want to deploy this web application when I use $ grails run-app at web context /awe. But this scenario is also applicable if for example we use SoapUI to create a WAR file with mocked web services and we want to access it from our Grails application.

I found two sources via Google: Grails, Tomcat and additional contexts and Run a Java web application within grails. It turns out Grails fires a configureTomcat event when we use $ grails run-app, with a Tomcat instance as argument. We can configure this Tomcat instance with additional information. With the addWebapp() method we can add an additional context to Tomcat. We can use a directory or WAR file and we must define the context name we want to use. And furthermore we can add extra directories that are added to the classpath of the additional web application. We must create a WebappLoader instance from Tomcat's classloader and then we can use the addRepository() method to add directories to the classpath.

For my use case I had a web application packaged as a WAR file: awe.war and it must be deployed with the context /awe. Furthermore extra configuration for the web application is done with a XML file found in the classpath, so we add an extra directory to the classpath of the web application.

import org.apache.catalina.loader.WebappLoader

eventConfigureTomcat = { tomcat ->
    // Use directory in the project's root for the
    // WAR file and extra directory to be included in the
    // web application's classpath.
    def aweWebappDir = new File(grailsSettings.baseDir, 'awe')
    def aweSharedDir = new File(aweWebappDir, 'shared')

    // Location of WAR file.
    def aweWarFile = new File(aweWebappDir, 'awe.war')

    // Context name for web application.
    def aweContext = '/awe'

    // Add WAR file to Tomcat as web application
    // with context /awe. 
    def context = tomcat.addWebapp(aweContext, aweWarFile.absolutePath)
    // Context is not reloadable, but can be 
    // if we want to.
    context.reloadable = false

    // Add extra directory to web application's
    // classpath for referencing configuration file needed
    // by the web application.
    def loader = new WebappLoader(tomcat.class.classLoader)
    loader.addRepository(aweSharedDir.toURI().toURL().toString())
    loader.container = context

    // Add extra loader to context.
    context.loader = loader
}

We start our development Grails environment with $ grails run-app and the web application is available at http://localhost:8080/awe/, next to the Grails' application path.

April 3, 2011

Groovy Goodness: See if List and Object Array are Equal

Groovy adds a lot of useful methods to standard JDK classes. For example Groovy adds the equals() method to List and Object[] so both can be compared. We must make sure the array is of type Object[] to make it work. Also the equals() method is added to arrays of type int.

def numbers1 = [1,2,3] as int[]
def numbers2 = [1,2,3] as int[]
def numbers3 = [1,2] as int[]

assert numbers1.equals(numbers2)
assert numbers2 == numbers1
assert !(numbers1.equals(numbers3))

def list = ['Groovy', 'Grails', 'Gradle']
def stringArray1 = ['Grails', 'Gradle', 'Groovy'] as Object[]
def stringArray2 = ['Groovy', 'Grails', 'Gradle'] as Object[]

assert list.equals(stringArray2)
assert list == stringArray2
assert !(list.equals(stringArray1)) // order matters
assert list != stringArray1