- Blog Xebia France - -

Posted By Guillaume Mathias One on Wednesday, May 7th, 2008 13:21 In Divers 2 Comments

We are going to see how create a mini - DSL to write expressions cron - used to scheduler batch - in a more legible way. Instead of writing "* */5 * * *?" we will grab "every (5.mn)", and right away you understand better that the batch will be thrown all 5mn!

Domain-Specific Language

Domain-Specific Language () is a language especially conceived for a definite domain. In opposition, non-specialised languages as the Popular dance allow to treat any types of problems but with a quantity of code often more important and a complexity more l v e.
Of course, for an application web, it is preferable to use of the Popular dance but for some definite problems the use of DSL can avoid you many lines of code or allows you at least to clarify an algorithm (and therefore to make it more maintenable). Ideally, the experts of domain can write themselves code with DSL.

Groovy

Groovy is a dynamic language based on JVM - that is scripts Groovy generate.class files interpreted by JVM - and DSL takes part well in the creation of internal DSL (who spreads an existent language, Groovy in this case). It allows him to integrate very easily with the Popular dance.
We are going to approach some of this functionality through a case in point: expressions cron. Coming from the world Unix, these expressions serve for scheduler of jobs via expressions of style "* 5/10 * * *?" which are certainly very short to write, but frankly not clear for the non-initiating. They are going to be make more legible...

The continuation of the article requires some bases in Groovy, also if you do not know what is a closure or if the Groovy syntax is you completely foreigner, I invite you to read the tutorial before beginning. Its syntax close to the Popular dance will allow to arrest language fast.

Expression Cron

A cron expression allows to describe a temporal complex combination of type "every last Friday from the month till 10 h".
She is composed of 6 fields + 1 optional field:

  • Seconds (0-59)
  • Minutes (0-59)
  • The hours (0-23)
  • Day of the month (1-31)
  • Month (1-12)
  • Day of the week (1-7 and SUN-SAT)
  • Year (optional) (1970-2099)

For every field it is possible to use following characters (example of the field the "hour "):

  • "* "-> every hour
  • "3,5 "-> at 3 h or 5 h
  • "3-5 "-> from 3 h till 5 h
  • "3/5 "-> every 5 h from 3 h

So only one gives information one of 2 fields days, other one must be in '? '.
There are some more other special characters which we will not approach here.

Some examples:

  • "0 0 12 * *?" mean "every day at 12 h"
  • "*/10 * * * *?" mean "all 10s"
  • "0 0 8-14? * MY" mean "every Monday, every hour, from 8 h till 14 h"

Creation of the mini - DSL

Let us begin by defining a class representing this type of expression:

class cron {
  G-string s, mn, h, day, month, dayOfWeek, year

  ToString G-string () {"$ {s?: '* '} $ {mn?: '* '} $ {h?: '* '} $ {day?: dayOfWeek? '? ': '* '} $ {month?: '* '} $ {dayOfWeek?:day? '? ': '? '} $ {year?:"}"}:'*'} ${dayOfWeek?:day?'?':'?'} ${year?:''}" }
}

Constructor with parametres called

We can already create expressions cron with the aid of parametres called:

def cron = new cron (day: '1 ', h: '8 ', mn: '0 ', s: '0 ')//all 1ers from the month till 8 h
println cron//shows '0 0 8 1 *?'

He does not need there to define with specific constructor, in Groovy it is possible to cross in parametre stocks of properties.
However, the use of a constructor remains rather technical (New), and it is necessary to specify units " mn " and " s " even when they belong to "0 ". We are going to try to make him more legible for an average user: "new cron (h: '8 ', mn: '0 ', s: '0 ')"-> "at (8.h)".

Interception of properties / methods

First of all, it is necessary to be able to interpret "8.h ". 8 Integer is, but h is not a property defined with the class Integer.
We are going to intercept the calls of properties on the class Integer with the aid of and return an object TimeUnit which will represent this unity of time:

All objects in Groovy have an attribute metaClass who allows introspection. The class Class also has this attribute but it is a bit particular: it is about ExpandoMetaClass more precisely. This metaClass allows to enrich the behaviour of a class by topping up to him methods, properties or there .

In example above we overloaded getProperty method () to intercept all properties called on Integer.
Delegate variable refers to the authority on which property is called.
And variable it, implicit in any closure, corresponds here in the name of called property.

The method at remains to define which takes in parametre TimeUnit and turns an expression Cron:

public Cron at (TimeUnit time) {
  cron cron = new cron ((Time.unit):time.value)

  //units <time.unit are positioned in 0
  def properties = [' s ', ' Mn ', 'h ', ' Day ', ' Month ', ' DayOfWeek ', ' Year ']
  int n = properties.indexOf (Time.unit)
  yew tree (n> 0)
    for (int i = 0; i <n; i ++)
      yew tree (! cron. (properties [i])) cron. (properties [i]) = '0' properties[i]) = '0'

  return cron
}

//at 8 h
println at (8.h)//shows '0 0 8 * *?'

The use of parentheses around (Time.unit) is necessary here to point out to Groovy that it is about a variable and about a character string" (Time.unit)".

Let us bring some change a bit in method to be possible cross several TimeUnit, as in "at 8 h, 10 h, and 14 h":

public Cron at (TimeUnit times) {
  cron cron = new cron ()

  times.each {TimeUnit time->
    yew tree (cron. (time.unit)) cron. (Time.unit) + = ', ' + time.value += ',' + time.value
    else cron. (Time.unit) = time.value

    //units <time.unit are positioned in 0
    def properties = [' s ', ' Mn ', 'h ', ' Day ', ' Month ', ' DayOfWeek ', ' Year ']
    int n = properties.indexOf (Time.unit)
    yew tree (n> 0)
      for (int i = 0; i <n; i ++)
        yew tree (! cron. (properties [i])) cron. (properties [i]) = '0' properties[i]) = '0'
  }

  return cron
}

//at 8 h, 10 h and 14 h
println at (8.h, 10.h, 14.h)//shows '0 0 8,10,14 * *?'

We are going to replace */5 now "* * * *?" by "every (5.mn)", what means "all 5mn"!

every public Cron (TimeUnit time) {
  return new cron ((Time.unit): '* / '+ time.value)
}

//all 5mn
println every (5.mn)//shows '* */5 * * *?'

Let us try now to combine these 2 criteria: they want to translate "at 2 h, every quarter" by "at (2.h) + every (15.mn)".
Certainly it is not perhaps the most elegant way, but it allows us to see the surcharge of operators.

Surcharge of operators

In Groovy practically all operators are possible be overloaded.

It is enough to overload method with the corresponding name. Here, we are going to overload the operator '+ ', that is to say method more:

//modification of the class Cron
class cron {

  //(properties were not repeated here)

  //surcharge of the operator '+'
  cron more (Cron other) {
    //for every property of type G-string of the class Cron:
    Cron.metaClass.properties.findAll {it.type == G-string}.each {
      def worth = Cron.metaClass.getProperty (this, it.name)
      def otherValue = Cron.metaClass.getProperty (other, it.name)
      Cron.metaClass.setProperty (this, it.name, compute (worth, otherValue))
    }
    return this
  }

  //concat ne 2 stocks of the same unit
  G-string compute (G-string s1, G-string s2) {
    yew tree (! s1) return s2//if one of the stocks are null they returns other one
    yew tree (! s2) return s1
    yew tree (s1 == '0 ') return s2//zero value is considered as empties, the other value crushes it
    yew tree (s2 == '0 ') return s1
    return s1 + ', ' + s2//if one gives information to one concat ne with one to 2 ','
  }
}

//at 2 h all 15mn
println at (2.h) + every (15.mn)//shows '0 */15 2 * *?'

We go through the properties of the class cron by using introspection on its metaClass, and for every property, they method is called compute which concat ne 2 stocks with a comma. Zero value is however considered to be space, it is therefore crushed if need. This realisation is far from managing all cases, but it is enough for the present examples.

We can then enrich our DSL to interpret every Friday "(at midnight)", by using constants:

//constants of the days of the week
final def monday = new cron (dayOfWeek:'MON', h: "'0", mn: "'0", s: "'0")
final def friday = new cron (dayOfWeek:'FRI', h: "'0", mn: "'0", s: "'0")

//on Monday (at midnight)
println monday//shows '0 0 0? * MY'

//new method every which takes cron in parametre
every (Cron crons) public Cron {
  cron sum = new cron ()
  crons.each {sum + = it}
  return sum
}

//equivalent in every Monday (at midnight)
println every (Monday)//shows '0 0 0? * MY'

//every Monday and Friday at 22 h
println every (monday, friday) + at (22.h)//shows '0 0 22? * MY, FRI'

You understood principle, it would be possible to continue enriching DSL, by adding for example the management of time intervals "from 8 h till 12 h". It could be made by overloading the operator Duration.minus () to write "at (8.h-12.h)". In fact main pressure is your imagination!

Summary

Let us return on points approached for this mini - DSL:

  • parametres called: give a rather intuitive aspect to the constructors
  • ExpandoMetaClass: the behaviour of a class allows to enrich
  • interception of properties / methods: allows to create objects in the runtime '5'
  • surcharge of operators: to redefine the behaviour of an operator

Other characteristics of Groovy are interesting for DSL:

  • : enriches a class locally
  • Builders: manipulation of trees

Builders is particularly powerful because they allow to write or parser every type of tree structure (XML, HTML, Swing, etc) in a very intuitive way. For example, the files of shape XML are often very verbose, it is possible to use a builder to simplify their writing. Steven Devijver shows us one for configurer Architecture rules.

The next stage would be to include code Groovy into an application Popular dance but it will make the object of another ticket...

Article printed from Blog Xebia France:

URL to article: / 2008 / 05 / 07 / introduction-aux-dsl-avec-groovy /

Click here to print.