Migrating code (read: legacy code) is not fun. It takes an enormous amount of planning and effort to get it across the line. While not the most exciting or motivating piece of work for developers, it does require determination and the right experience to migrate legacy code to new library versions. Joda-Time to java.time is one such migration that requires meticulous planning and execution.
If your Java project started its life out prior to Java SE 8, and uses date/time processing, then it probably used Joda-Time - an excellent library and a de-facto standard to handle date and time functions prior to SE 8. If your project still uses Joda-Time but would like to migrate to java.time then read on.
The release of Java SE 8 included a new and improved standard Date and Time API commonly referred to as java.time (JSR-310). The Joda-Time project now recommends migrating to java.time (JSR-310).
While java.time (JSR-310) was heavily inspired by Joda-Time, it is not backwards compatible and concepts and terminologies have changed. Thats why migrating from Joda-Time to java.time requires careful attention on each and every line of code that you change. This can be time-consuming and would almost make you wish there was an easier and automated way to migrate.
There is a better way to migrate and we’ve created it using Sensei - an IntelliJ plugin to automatically perform code transformations as per the recipes (rules) defined by you. Spend your time defining reusable recipes, rather than performing repetitive migration tasks. The automation will not not only transform your legacy Joda-Time code, but also help teams follow the guidelines right there in the IDE as they write new code.
To help you get a head start, we have created a public Sensei cookbook Standardization on java.time (JSR-310) which includes recipes to migrate from Joda-Time to java.time in a less painful way. This is a growing set of recipes that we will continue to expand to add more coverage with more recipes.
Here is an example of a sample migration that might help you see how Sensei makes it painless to migrate legacy code.
From repetitive manual migration to automated code transformations Let’s look at an example of creating a new DateTime which demonstrates a few hidden traps when migrating a single line of code from Joda-Time to java.time. We will then look at one of our Sensei recipes from our Standardization on java.time (JSR-310) cookbook and show how it captures all this information, so that this same migration can be re-used again and again by any developer.
In this example, we are constructing a Joda-Time DateTime from 7 int arguments representing values of the DateTime fields.
How do we migrate this to a java.time equivalent?
The javadoc in Joda-Time for this constructor says:
Constructs an instance from datetime field values using ISOChronology in the default time zone. At first, we might assume there is DateTime class in java.time, but there isn’t one. If you google ‘migrate from joda time to java time’ you will most likely find Stephen Colebourne’s blog post Converting from Joda-Time to java.time .
This gives you a good start, and points us in the direction of using java.time.ZonedDateTime or java.time.OffsetDateTime. Here is our first question, which one do I use? Probably ZonedDateTime based on Stephen’s comments.
Looking up the ZonedDateTime javadoc , we can’t see any constructors at all. Returning to Stephen’s blog post we read further down:
Construction . Joda-Time has a constructor that accepts an Object and performs type conversion. java.time only has factory methods, so conversion is a user problem, although a parse() method is provided for strings. So there must be a static factory method, searching the static methods we find one that looks pretty close, but it’s not exactly the same.
It has 7 int parameters like our original Joda-Time DateTime constructor, however if you don’t pay attention you will miss an important detail. The 7th parameter no longer represents milliseconds, it expects nanoseconds instead, this is because java.time has increased precision over Joda-Time and measures instants to the nano-second. An important detail that you could have easily missed. Additionally, this method expects a ZoneId, so it makes you wonder as to why you didn’t need one before and why do you need it now.
Remembering the javadoc of our original constructor which mentioned it would use the default time zone, maybe there is a way to get the default ZoneId?
The javadoc for ZoneId does not tell us about any constructors listed, but looking at the static methods we see we can use systemDefault()
Now that we have sorted out the ZoneId, what should we do about our milliseconds to nanoSeconds conversion? Maybe we can use java.util.concurrent.TimeUnit to perform the conversion.
This method returns a long, and our method expects an int, so now we have a conversion problem to solve as well. Maybe we could try something simple. A multiplication?
This will work but does look a little out of place. If you haven’t noticed already, we have spent a considerable amount of time and effort to migrate a single line of code. But as you can imagine, we have many such edits to do by hand and it does not get better.
However, if we were to look a little harder at java.time API, we can discover a solution that looks a little bit more fluent.
Although ZonedDateTime has no obvious way to set the Milliseconds, it can be done using the with(TemporalField field, long newValue) method , using ChronoField.MILLI_OF_SECOND as the TemporalField.
And the java doc mentions that it will perform the Conversion to Nanoseconds for us:
When this field is used for setting a value, it should behave in the same way as setting NANO_OF_SECOND with the value multiplied by 1,000,000. So we can simply specify 0 for our nanoseconds in the factory method, and then use the with method to create a ZonedDateTime that has all the original values as well as the milliseconds.
Looking at our final result, it looks like we have only changed a single line of code, it doesn’t really show the effort that went researching just one migration!
Create a recipe to migrate faster and easily Sensei provides a way for us to share this hard-won information with other developers. By creating a recipe that captures all these requirements, it will allow Sensei users to perform this migration with the click of a mouse.
A Sensei recipe consists of 3 main sections:
Metadata Search AvailableFixes Let us look at a Sensei recipe (can also be viewed as a YAML recipe) that will help us migrate this call into its java.time equivalent.
DateTime foo = new DateTime(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond); Metadata Section The metadata section contains information about the recipe and how it should be used.
Search Section The search section of a Sensei recipe specifies what code elements this recipe should apply to.
search: instanceCreation: args: 1: type: int 2: type: int 3: type: int 4: type: int 5: type: int 6: type: int 7: type: int argCount: 7 type: org.joda.time.DateTime
In this search section we see that we are:
Searching for an instanceCreation, i.e. a usage of a Constructor. Note: there are many other search targets available The constructor should have 7 arguments, this is specified by the argCount property args 1-7 should be of type intWe are searching for constructors of the type org.joda.time.DateTime
Available Fixes Section The availableFixes section can specify one or more fixes that can be applied to the matching code element. Each fix can have multiple actions, and in our case we have a single fix, that performs 2 actions.
The name of the fix is shown to the user in the ‘Quickfixes’ menu, and describes what will happen if the user applies this quickfix The actions list shows what actions will be performed by this quickfix The rewrite action will rewrite the code element using a mustache template. It can make use of variables and string replacement functions. The modifyAssignedVariable action will check to see if this constructor is being used to assign the value to a variable. If so, this action will modify the variable to be declared as the type specified by type
Using the recipe to do code transformation With our recipe is written and enabled, it scans our code and highlights the segments to which it can be applied.
In the screenshot below we can see the target constructor has been marked by Sensei. Hovering over the marked constructor shows us the Recipe shortDescription and the Quickfix option Migrate to java.time.ZonedDateTime
After we select the Migrate to java.time.ZonedDateTime quickfix, the code is transformed according to the actions we specified in the recipe.
A one-time migration and uniform coding practices across teams - with Sensei We can see from our example above, the migration of a single line of code can involve hard-won knowledge. Sensei can turn that knowledge into actionable recipes or cookbooks that can be shared within teams. You may plan a one-time migration sprint or take the approach of doing incremental instant transformations to java.time as and when you come across Joda-Time code. You can enable/disable recipes as a way to do migrations in logical stages or steps and even, expand or reduce the scope of the files scanned by Sensei - the flexibility that makes code migrations less painful.
Library migration is just one example of the many ways Sensei can be used to standardize your projects. You can always be on the lookout for anti-patterns or certain manual code transformations that you frequently come across in pull requests or as you code yourself. If you have a set of coding guidelines that are often missed by developers then you could convert the guidelines into recipes - enabling developers to apply approved code transformations with confidence.
If you have any questions, we’d love to hear from you! Join us on Slack at: sensei-scw.slack.com