I am trying to understand the following code snippet following site.
<% refChanges.getCommits(repository).each { commit -> %>
- ${commit.author.name} | ${commit.displayId} | ${commit.message} | ${commit.authorTimestamp}
<% } %>
The script is using a getCommits method, but when I look at the documentation for the RefChange interface I do not see any such method.
I consider myself an expert Java developer, but I have no workable knowledge in Groovy, so I assume that I am misunderstanding Groovy or the BitBucket documentation (or both).
In Groovy it's possible to add methods to a class or interface at run-time via meta-programming. Since the RefChange interface does not include getCommits(), it must be that the method is being added after-the-fact. Based on their example code, it looks like they're using the meta-class.
Where is getCommits()?
For example, in Groovy the Collection interface gets the method findAll() (along with many other methods). I can confirm this as follows:
assert Collection.metaClass.metaMethods*.name.contains('findAll') == true
The code above grabs the names of all the meta methods and then uses contains() to see if a match is found. You can confirm the same for getCommits() in a similar way:
assert Collection.metaClass.metaMethods*.name.contains('getCommits') == true
Note that I specified Collection rather than RefChange because refChanges is a Collection of RefChange. And so I think Atlasssian stuck getCommits() into Collection as a convenience method.
How does it work?
To understand what's going, I'll remove the templating code:
refChanges.getCommits(repository).each { commit ->
"${commit.author.name} | ${commit.displayId} | ${commit.message} | ${commit.authorTimestamp}"
}
getCommits() returns a Collection of com.atlassian.bitbucket.commit.Commit.
Object.each(Closure) is added by the Groovy GDK (yes, into the Object class) and it calls the Closure repeatedly; each time with an element of the Collection.
Although it's not apparent in the example code, the line within the each(Closure) is a GString. Basically the expressions within the ${...} are evaluated and the whole thing is concatenated into a String.
Related
Sorry to all you Groovy dudes if this is a bit of a noob question.
In SOAPUI, i can create a Groovy script where i can define an arbitrary variable to the run context to retrieve at a later time.
context.previouslyUndefinedVariable = 3
def num = context.previouslyUndefinedVariable
What feature of Groovy allows previously undefined variables to be added to an object like this? I would like to learn more about it.
Many thanks in advance!
Groovy has the ability to dynamically add methods to a class through metaprogramming.
To learn more, see:
What is Groovy's MetaClass used for?
Groovy Goodness: Add Methods Dynamically to Classes with ExpandoMetaClass
Runtime and compile-time metaprogramming
The accepted answer is a bit of a poor explanation for how SoapUI is doing it.
In this case, context is always an instance of some SoapUI library java class (such as WsdlTestRunContext), and these are all implementations of Map. You can check context.getClass() and assert context in Map.
When you look up a property on a Map, Groovy uses the getAt and putAt methods. There are various syntaxes you can use. All of these are equivalent:
context.someUndef
context.'someUndef'
context[someUndef]
context['someUndef']
context.getAt('someUndef')
And
context.someUndef = 3
context.'someUndef' = 3
context[someUndef] = 3
context['someUndef'] = 3
context.putAt('someUndef', 3)
I like to use any of the above that include quote marks, so that Groovy-Eclipse doesn't flag it as a missing property.
It's also interesting that Groovy looks for a getAt() method before it checks for a get method being referred to as a property.
For example, consider evaluating "foo".class. The String instance doesn't have a property called class and it also doesn't have a method getAt(String), so the next thing it tries is to look for a "get" method with that name, i.e. getClass(), which it finds, and we get our result: String.
But with a map, ['class':'bar'].class refers to the method call getAt('class') first, which will be 'bar'. If we want to know what type of Map it is, we have to be more specific and write in full: ['class':'bar'].getClass() which will be LinkedHashMap.
We still have to specify getClass() even if that Map doesn't have a matching key, because ['foo':'bar'].class will still mean ['foo':'bar'].getAt('class'), which will be null.
I've seen a bunch of questions related to this subject, but none of them offers anything that would be an acceptable solution (please, no loading external Groovy scripts, no calling to sh step etc.)
The operation I need to perform is a oneliner, but pipeline limitations made it impossible to write anything useful in that unter-language...
So, here's minimal example:
#NonCPS
def encodeProperties(Map properties) {
properties.collect { k, v -> "$k=$v" }.join('|')
}
node('dockerized') {
stage('Whatever') {
properties = [foo: 123, bar: "foo"]
echo encodeProperties(properties)
}
}
Depending on whether I add or remove #NonCPS annotation, or type declaration of the argument, the error changes, but it never gives any reason for what happened. It's basically random noise, that contradicts the reality of the situation (at times it would claim that some irrelevant object doesn't have a method encodeProperties, other times it would say that it cannot find a method encodeProperties with a signature that nobody was trying to call it with (like two arguments instead of one) and so on.
From reading the documentation, which is of disastrous quality, I sort of understood that maybe functions in general aren't serializable, and that is why you need to explain this explicitly to the Groovy interpreter... I'm sorry, this makes no sense, but this is roughly what documentation says.
Obviously, trying to use collect inside stage creates a load of new errors... Which are, at least understandable in that the author confesses that their version of Groovy doesn't implement most of the Groovy standard...
It's just a typo. You defined encodeProperties but called encodeProprties.
I need to run some code whenever a property value is retrieved, so naturally it made sense to define the getProperty method in my class. This method will get automatically called whenever a property value is retrieved. Here's roughly what I have in my class:
class MyClass
{
def getProperty(String name)
{
// Run some code ...
return this.#"${name}"
}
}
The problem with the above method occurs when someone tries to make the following call somewhere:
MyClass.class
This call ends up in the getProperty method looking for a property named "class", however, there is not actual property named "class" so we get a MissingFieldException.
What would be the correct way to implement running code whenever a property value is retrieved and deal with these kind of situtations.
Best is not to have a getProperty method if not needed. If you need one and you want to fall back on standard Groovy logic, then you can use return getMetaClass().getProperty(this, property), as can be found in GroovyObjectSupport. This will cover more than just fields.
This seems to be a common problem with this method. Map has the same issue. The developers of groovy got around the problem with Map by saying you need to use getClass() directly.
I have a method that takes a string and a closure, which I include in my plugin convention:
def someMethod( String obj, Closure closure) {
println('HERE I AM')
confFileTree = project.fileTree( obj, closure )
}
From a Junit test I call it like so:
project.convention.plugins.license.licenseFiles( 'src') {
include "main/java/**"
include "main/resources/*.properties"
exclude "**/Licensed.java"
}
I know the method is called because 'HERE I AM' is printed. But I then get an error that says:
org.gradle.api.internal.MissingMethodException:
Could not find method fileTree() for arguments
[src, nl.javadude.gradle.plugins.license.tasks.LicenseTaskTest$_shouldScanFilesForLicenseWithExclude_closure1#3cbdb6ae]
on root project 'test'.
I should state that this code originally just called the Closure form of fileTree, with "from 'src'" in the closure, which works fine, but Gradle milestone 8 is telling me that it is a deprecated method.
Are you sure that the test is running against m8? In any case, here are a few suggestions for improvement (since I already know what you are trying to achieve):
I don't think you want to construct your own file tree. You just want the user to pass a 'filter' closure (like in your example) which you then apply to the source directory set (e.g. sourceSets.main.java) with the FileTree.matching(Closure) method. You'll get back a new file tree with the filter applied.
I recommend to use an extension rather than a convention object
You don't need the long-winded syntax when accessing convention objects or extensions from Groovy code. In your unit test example, you can just say project.licenseFiles(...) {...}.
I want to let users supply a groovy class with a property that is a file-selector closure which I pass on to AntBuilder's 'copy' task:
class Foo {
def ANT = { fileset(dir:'/tmp/tmp1') }
}
in my code, I pick up the ANT property as 'fAnt' and pass to Ant:
ant.copy(todir:'/tmp/tmp2', fAnt)
This works - but, if the user passes in an empty closure (def ANT={}) or with a selector that doesn't select anything (maybe the fileset dir doesn't exist) then it blows up. I tried surrounding the ant copy with a try-catch to catch the InvokerInvocationException, but somehow the exception comes through anyway ... while I'm tracking that down, is there a way to read back a groovy Closure's contents as a string, or to test if it's empty?
In short: No. You can't decompile a closure in a meanngful way at runtime. If it's user supplied, the Closure could even be a Java class.
Long answer: If you want to do a lot of work, you might be able to, but it's probably not worth it. The Groovy parser is part of the API, so if you have access to the source, you can theoretically examine the AST and determine if the closure is empty. Look into the SourceUnit class.
It's almost certainly not worth the effort though. You're better off catching the exception and adding a helpful message like "You may have passed an empty closure or invalid fileset".
One mystery solved - the exception I need to catch is org.apache.tools.ant.BuildException - so I can just catch that to trap errors, but the original question remains - is there a way to examine a Closure's contents?