8

I am trying to update date fields in mongo that require an ISODate format. In mongo, it looks like this:

"crDt" : ISODate("2013-08-19T17:21:57.549Z")

The Java framework I am using has me restricted to using strings as my test parameters, so I am trying to use that string with a DateTimeFormatter to get it into the correct ISODateTimeFormat and then pass that into mongo. I cannot just pass in a string that looks like what I have above. Trying to do so screws up the field in mongo. The relevant bits of Joda-Time code I am using look like this:

//I can't get this right.
String crDt = "2013-01-19T15:28:58.851Z";

DateTimeFormatter parser = ISODateTimeFormat.dateHourMinuteSecondMillis();

parser.parseDateTime(crDt);

// this method updates the record in mongo. This method totally works, so no 
// point in pasting it here, I just can't get the parser object correct to be 
// in the correct format once inserted, it needs to be the correct ISODate form.
mongo.setCrDt(recordId, parser);

And when the code runs I get errors like these from the .parseDateTime method:

java.lang.IllegalArgumentException: Invalid format: "2013-01-19T15:28:58.851Z" is malformed at "Z"
    at org.joda.time.format.DateTimeFormatter.parseDateTime(DateTimeFormatter.java:866)

I can tell the string I am giving is not correct to get things parsed. I've tried leaving off the Z, I've tried other combos, but each time it says it's malformed. So basically, what does my starting string need to be to get the .parseDateTime to work and give me an object that looks correct?

EDIT:

Updated to try the suggestions provided below. The issue I run into now is an IllegalArgumentException, can't serialize class org.joda.time.DateTime. So it appears persisting joda time objects in a no-go? I also looked at the other suggestion, looking into mapper frameworks like Spring Data. It looks like there is a whole lot more that needs to go into this. Is there really no simple way to persist this into mongo?

EDIT2:

OK, I think I have it now. I might not have a total grasp of all the mechanics at play, but BasicDBObjects won't play nice with DateTime. Date objects seem to be the only way to go, at least in the implementation I'm dealing with. I did the following:

DateTimeFormatter parser = ISODateTimeFormat.dateTime();
DateTime result;
Date newResult;
result = parser.parseDateTime(crDt);
newResult = result.toDate();

I then passed in newResult for the BasicDBObject to then update the record in mongo. It works fine, and the record is updated correctly.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Benny
  • 242
  • 1
  • 5
  • 17

4 Answers4

7

Your input string format is correct, as long is that is intended to represent UTC.

Change your parser to use the one that matches this format:

DateTimeFormatter parser = ISODateTimeFormat.dateTime();

The rest of your question doesn't make much sense to me. You shouldn't pass the parser, but rather the return value from parseDateTime, which you don't appear to be capturing.

DateTime result = parser.parseDateTime(crDt);

mongo.setCrDt(recordId, result.toDate());

Whether or not that last line will work depends on what that function accepts.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • I now get an IllegalArgumentException when trying to serialize the DateTime object. Similar And based on that other stackoverflow link, it sounds like I need to be utilizing something like Spring for MongoDB. Not sure where to start with this, all I need to do is just update some fields in mongo. – Benny Aug 19 '13 at 20:44
  • A lot of it depends on exactly how you persisting the object, but you didn't show the internals (or relevant parts) of `setCrDt` method, so it's difficult to respond. It may be as simple as calling `.toDate()` on the result to get a Java `Date` if your implementation understands that. Or it might be much more complex. Hard to say without more detail. – Matt Johnson-Pint Aug 19 '13 at 20:46
  • According to [this documentation](http://docs.mongodb.org/ecosystem/drivers/java-types/#JavaTypes-Dates%2FTimes), a `java.util.Date` is what is expected by the MongoDB Java driver, so passing `result.toDate()` should do the trick. – Matt Johnson-Pint Aug 19 '13 at 20:52
  • .toDate() was the magic. It was pretty simple after all. Thanks for all the help! – Benny Aug 19 '13 at 21:23
  • This is the answer to my question. THanks – black sensei Sep 07 '13 at 17:58
5

I solved this by adding an "Encoding Hook" in the constructor of the Service class where I do the updates to MongoDB. This will allow you to use org.joda.time.DateTime in your code and that will be saved as java.util.Date in MongoDB.

MyService.java

@Inject
public MyService(com.mongodb.Client client) {
      BSON.addEncodingHook(DateTime.class, new JodaTimeTransformer());
      BSON.addDecodingHook(Date.class, new JodaTimeTransformer());
      this.mongoClient = mongoClient;
}

JodaTimeTransformer.java

import java.util.Date;

import org.joda.time.DateTime;

public class JodaTimeTransformer implements org.bson.Transformer {

    @Override
    public Object transform(Object o) {
        if(o instanceof DateTime) {
            return ((DateTime)o).toDate();
        }
        else if(o instanceof Date) {
            return new DateTime((Date) o);
        }
        throw new IllegalArgumentException("JodaTimeTransformer can only be used with DateTime or Date");
    }

}
Miguel Reyes
  • 2,444
  • 1
  • 21
  • 11
3

The answer by Matt Johnson is correct. But it could be even simpler: Pass the (ISO 8601) string directly to constructor of DateTime. No need for a formatter.

Pay attention to time zone. A DateTime object in Joda-Time truly knows its own assigned time zone, unlike a java.util.Date object. Do you want your DateTime object to be assigned the JVM’s default time zone, no time zone (UTC), or a specific time zone?

For a date-time assigned the default time zone.

DateTime dateTime = new DateTime( "2013-01-19T15:28:58.851Z" );

For a date-time assigned UTC/GMT (no time zone offset).

DateTime dateTime = new DateTime( "2013-01-19T15:28:58.851Z", DateTimeZone.UTC );

For a date-time assigned a specific time zone.

DateTime dateTime = new DateTime( "2013-01-19T15:28:58.851Z", DateTimeZone.forId( "Europe/Paris" ) );
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

Most of these answers are ancient. The Mongo driver is much newer and has changed dramatically. Here is an answer as of March 6, 2019 - using the most recent version of the Mongo Java driver - version 3.10.1, using most recent version of Joda time - 2.10.1. I intentionally use fully qualified class names so there is no confusion on the libraries in use. As such, there is no need for any import statements.

**

Edit 2019-03-09:

Feedback from user @OleV.V. (see comments below) indicate perhaps Joda Time is less favorable over the Java 8 java.time libraries. Upon investigation I find the current MongoDB Java driver supports the java.time.Instant.now() method properly and saves an ISODate without the need of a codec. The information provided here illustrates how to add a custom codec to the driver. For this reason alone I believe there is value in this answer.

**

My answer is derived from work from SquarePegSys BigDecimalCodec.java found at https://gist.github.com/squarepegsys/9a97f7c70337e7c5e006a436acd8a729, the difference is their solution is geared towards supporting big decimal values, my solution is geared toward Joda DateTime compatibility.

I like to provide the output of the program first, before showing source code. This way you can evaluate if the output is providing the solution you are seeking before investing time digesting and understanding the code. Again, the point is to save a date value as an ISODate datatype in mongoDB using Joda time, i.e., so the saved DateTime is not saved as a string.

I am using Maven to build. I am running Ubuntu 18.04LTS.

$ mvn -version
Apache Maven 3.5.2
Maven home: /usr/share/maven
Java version: 10.0.2, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-43-generic", arch: "amd64", family: "unix"

Build the program:

cd <directory holding pom.xml file>
mvn package

Run the program:

$ java -jar Test.jar 
Mar 06, 2019 5:12:02 PM com.mongodb.diagnostics.logging.JULLogger log
INFO: Cluster created with settings {hosts=[127.0.0.1:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms', maxWaitQueueSize=500}
Mar 06, 2019 5:12:03 PM com.mongodb.diagnostics.logging.JULLogger log
INFO: Opened connection [connectionId{localValue:1, serverValue:9}] to 127.0.0.1:27017
Mar 06, 2019 5:12:03 PM com.mongodb.diagnostics.logging.JULLogger log
INFO: Monitor thread successfully connected to server with description ServerDescription{address=127.0.0.1:27017, type=STANDALONE, state=CONNECTED, ok=true, version=ServerVersion{versionList=[4, 0, 6]}, minWireVersion=0, maxWireVersion=7, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=3220919}
Mar 06, 2019 5:12:03 PM com.mongodb.diagnostics.logging.JULLogger log
INFO: Opened connection [connectionId{localValue:2, serverValue:10}] to 127.0.0.1:27017

Query results using mongo shell:

MongoDB > db.testcollection.find().pretty()
{
        "_id" : ObjectId("5c806e6272b3f469d9969157"),
        "name" : "barry",
        "status" : "cool",
        "number" : 1,
        "date" : ISODate("2019-03-07T01:05:38.381Z")
}

Source Code

There are a total of 4 classes implemented (the pom.xml file is just a build tool file)...

  1. Main
  2. Transformer
  3. Provider
  4. Codec

pom.xml

<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test.barry</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>test</name>
    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${basedir}</outputDirectory>
                            <finalName>Test</finalName>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>test.barry.Main</mainClass>
                                </transformer>
                            </transformers>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
            <version>3.10.1</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.1</version>
        </dependency>
    </dependencies>
</project>

Main.java

package test.barry;

public class Main {

    public static void main(String[] args) {

        java.util.ArrayList<com.mongodb.ServerAddress> hosts = new java.util.ArrayList<com.mongodb.ServerAddress>();
        hosts.add(new com.mongodb.ServerAddress("127.0.0.1", 27017));

        com.mongodb.MongoCredential mongoCredential = com.mongodb.MongoCredential.createScramSha1Credential("testuser", "admin", "mysecret".toCharArray());

        org.bson.BSON.addEncodingHook(org.joda.time.DateTime.class, new test.barry.DateTimeTransformer());
        org.bson.codecs.configuration.CodecRegistry codecRegistry = org.bson.codecs.configuration.CodecRegistries.fromRegistries(
                org.bson.codecs.configuration.CodecRegistries.fromProviders(
                    new test.barry.DateTimeCodecProvider()
                ), com.mongodb.MongoClient.getDefaultCodecRegistry()
        );

        com.mongodb.MongoClientSettings mongoClientSettings = com.mongodb.MongoClientSettings.builder()
            .applyToClusterSettings(clusterSettingsBuilder -> clusterSettingsBuilder.hosts(hosts))
            .credential(mongoCredential)
            .writeConcern(com.mongodb.WriteConcern.W1)
            .readConcern(com.mongodb.ReadConcern.MAJORITY)
            .readPreference(com.mongodb.ReadPreference.nearest())
            .retryWrites(true)
            .codecRegistry(codecRegistry)
            .build();

        com.mongodb.client.MongoClient client = com.mongodb.client.MongoClients.create(mongoClientSettings);
        com.mongodb.client.MongoDatabase db = client.getDatabase("testdb");
        com.mongodb.client.MongoCollection<org.bson.Document> collection = db.getCollection("testcollection");

        // BUILD UP A DOCUMENT
        org.bson.Document document = new org.bson.Document("name", "barry")
            .append("status", "cool")
            .append("number", 1)
            .append("date", new org.joda.time.DateTime());

        collection.insertOne(document);
    }
}

DateTimeCodec.java

package test.barry;

public class DateTimeCodec implements org.bson.codecs.Codec<org.joda.time.DateTime> {
    @Override
    public void encode(final org.bson.BsonWriter writer, final org.joda.time.DateTime value, final org.bson.codecs.EncoderContext encoderContext) {
        writer.writeDateTime(value.getMillis());
    }

    @Override
    public org.joda.time.DateTime decode(final org.bson.BsonReader reader, final org.bson.codecs.DecoderContext decoderContext) {
        return new org.joda.time.DateTime(reader.readDateTime());
    }

    @Override
    public Class<org.joda.time.DateTime> getEncoderClass() {
        return org.joda.time.DateTime.class;
    }
}

DateTimeCodecProvider.java

package test.barry;

public class DateTimeCodecProvider implements org.bson.codecs.configuration.CodecProvider {
    @Override
    public <T> org.bson.codecs.Codec<T> get(final Class<T> classToVerify, final org.bson.codecs.configuration.CodecRegistry registry) {
        if (classToVerify == org.joda.time.DateTime.class) {
            return (org.bson.codecs.Codec<T>) new DateTimeCodec();
        }

        return null;
    }
}

DateTimeTransformer.java

package test.barry;

public class DateTimeTransformer implements org.bson.Transformer {
    @Override
    public Object transform(Object objectToTransform) {
        org.joda.time.DateTime value = (org.joda.time.DateTime) objectToTransform;
        return value;
    }
}

Conclusion

The java world seems to be gravitating towards Joda time. Its a nice library and provides relief for common date/time needs. My guess is Mongo will natively support this library but for now we must help it along.

Quick note: I attempted to use the most modern mongoDB classes, but in class Main.java, I refer to an older library method - com.mongodb.MongoClient.getDefaultCodecRegistry() as I could not find it in com.mongodb.client.MongoClient. If you identify how to use com.mongodb.client.MongoClient instead please add a comment...

barrypicker
  • 9,740
  • 11
  • 65
  • 79
  • 1
    While this is a thorough and knowledgeable answer — thank you! — the Java world is certainly moving *away* from Joda-Time towards java.time, the modern Java date and time API. Quote from the Joda-Time homepage: “Note that Joda-Time is considered to be a largely “finished” project. No major enhancements are planned. If using Java SE 8, please migrate to `java.time` (JSR-310).” Joda-Time was nice, but most of us find java.time even better. – Ole V.V. Mar 07 '19 at 10:35
  • 1
    @OleV.V. - thanks for the heads up! I will investigate java.time. – barrypicker Mar 08 '19 at 19:36