Home > Software design >  How do you reference a function with the same name as a property?
How do you reference a function with the same name as a property?

Time:07-07

Question

How can I reference this top-level function from within the data class? Or is Java's encapsulation of a class restrictive to the point that you cannot reach beyond the current class?

Code


def String branchName() {
  return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1];
}

public DeployConfig implements IDeployConfig {
  public DeployConfig(IDeployConfig config) {
    this._appName = config.app;
    this._gitUrl = config.gitUrl;
    // ... et cetera
  }
  
  public String getBranchName() {
    return branchName()
  }
}

Background

I'm trying to define a data class that represents our standard Jenkinsfile configuration, in an attempt to make our pipeline more testable, and less "cross your fingers and hope it didn't break anything". Toward that goal, here is a snippet of that implementation.

Now, the property getter I'm trying to write doesn't know the actual branch being built when the object is constructed, because that's derived from the Map<String, String> returned by checkout scm which gets instantiated at runtime. We assign the GIT_BRANCH out to the global environment env.GIT_BRANCH so that it can be referenced elsewhere.

Miscellaneous

To the would-be suggestion of putting the target branch in the Jenkinsfile, that defeats the purpose of the Jenkinsfile being an instruction set for a job with Git configurations assigned, such as a multi-branch job with a shared Jenkinsfile.

Other Code

To give some context about what I mean about the checkout scm command happening after the construction of DeployConfig, the pipeline roughly resembles this:

// ./pipeline-library/vars/deploy.groovy
#!/usr/bin/groovy
def call(Closure body) {
  def config = [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = config
  body()

  environmentVariables(config) // assigns certain keys to global env

  if (env.IS_PROD) {
    deployProd(config)
  }
  else {
    deployNonProd(config)
  }
}

// ./pipeline-library/vars/deployNonProd.groovy
#!/usr/bin/groovy

def call(Map config) {
  // local variable declarations
  
  pipeline {
    agent { 
      label 'some-configuration-name'
    }
    
    environment {
      // shared environment variables
    }

    options {
      // configured options, like timestamps and log rotation
    }
    
    stages {
      stage('Checkout') {
        steps {
          def gitInfo = checkout scm
          env.GIT_BRANCH = gitInfo.GIT_BRANCH
        }
      }
      
      // additional stages
    }
  }
}

Edits

Edit: The idea behind the property that calls the top-level function is a computed property that gets called later in the pipeline, after the checkout scm command has been executed. The DeployConfig would be constructed before the pipeline runs, and so the branch is not known at that time.

CodePudding user response:

So I solved the problem for myself, but it's arguably a less than ideal solution to the problem. Basically, here's what I had to do:

First, I created a getter getGetBranchName and setter setGetBranchName on the class of type Closure<String> and it had a backing field _getBranchName. I also created a property getBranchName of type String that returned the result of this._getBranchName().

Second, if the incoming Map has a property branchName, then I set the value this._getBranchName = () -> { return config.branchName } so that I am referencing the getter of an outer object.

Third, as a final check, I assign the global function signature from Jenkins after constructing the DeployConfig object. That all looks like the below code (Note: ellipses are used to indicate more code unrelated to the specific solution):

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

import com.domain.jenkins.data.DeployConfig
import com.domain.jenkins.data.GitUrl
import com.domain.jenkins.exceptions.InterruptException
import com.domain.jenkins.io.FileSystem
import com.domain.jenkins.pipeline.PipelineJob
import com.domain.jenkins.pipeline.PipelineStage


class Program {
  static FileSystem fs
  static PipelineJob pipeline
  static Map<String, String> env
  static Map jenkinsfile
  
  static {
    fs = new FileSystem()
    pipeline = new PipelineJob()
    env = [:]
    jenkinsfile = [ ... ]
  }

  static String branchName() {
    return ((env.GIT_BRANCH ?: 'master') =~ /(?i)^(?:origin\/)?(.*)/)[0][1]
  }

  static void main(String[] args) {
    println 'Initialize pipeline'
    pipeline = new PipelineJob()
    println 'Initialize configuration'
    DeployConfig config = new DeployConfig(jenkinsfile)
    println new JsonBuilder(config.toMap()).toPrettyString()
    println 'Assign static method as getBranchName method'
    config.getBranchName = () -> { return branchName() }
    println 'Assign environment variables to global env'
    env << config.environmentVariables

    ...
  }
}

And the DeployConfig class accepts that using the following (Note: most of the related code is not included for brevity's sake):

package com.domain.jenkins.data

import com.domain.jenkins.interfaces.IDeployConfig
import com.domain.jenkins.interfaces.IMappable
import com.domain.jenkins.interfaces.Mappable

class DeployConfig extends Mappable implements IDeployConfig, IMappable {
  private Closure<String> _getBranchName

  DeployConfig() {
    this.branchName = 'master'
  }

  DeployConfig(Map options) {
    this.branchName = options.branchName
  }

  String getBranchName() {
    return this._getBranchName()
  }
  
  void setBranchName(String value) {
    if(this._getBranchName == null) {
      this._getBranchName = () -> { return 'master' }
    }
    
    if(value) {
      this._getBranchName = () -> { return value }
    }
  }
  
  void setGetBranchName(Closure<String> action) {
    this._getBranchName = action
  }
}
  • Related