Skip to main content

XPages Date Field Issue: Solving the One-Day Jump on Every Save

 A user reported a very strange issue - when a document with a date field is saved, it changes the value one day to the past. With every save. But only for some dates, not all. It turned out to be a mystery that goes deep into XPages and Notes/Java APIs.

I've posted a sample on OpenNTF Discord and Serdar tried it on his server - no issue. But he uses the GMT zone and I have CET (Windows set to UTC+1 - Amsterdam, Berlin... to be precise).

To cut it short, the issue is caused by daylight saving interpretation between Notes and Java. The date fields (because XPages have no notion of real date-only fields) are stored with 00:00 time component and for some dates the conversion back to Java Date resulted in 23:00 on the previous day. XPages that get the date component as String for the input field, which is then saved back as a previous day during document save.

The app is full of date fields and I couldn't add custom logic to every save operation, so I tried to fix it at XPages converter level. The problem actually happens before that as the Date object in the converter is already wrong, but if I could intercept had massage the value, the rest of the application can remain unaffected. 

API test

But before fixing the problem, we need to see how big the problem is. I was able to create a Java agent with a loop that reports all dates that "jump". You can try it on your machine (and your time-zone).

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import lotus.domino.AgentBase;
import lotus.domino.AgentContext;
import lotus.domino.DateTime;
import lotus.domino.Session;

public class JavaAgent extends AgentBase {

    public void NotesMain() {

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
            SimpleDateFormat sdf2y = new SimpleDateFormat("dd.MM.yy");
            Session session = getSession();

            AgentContext agentContext = session.getAgentContext();

            Calendar cal = new GregorianCalendar(2024, 7, 21);
            for (int i = 0; i < 50000; i++) {
                // create a new date and let Notes decide about the DST
                DateTime dt = session.createDateTime(sdf.format(cal.getTime()) + " 00:00:00");
                Date date = dt.toJavaDate();
                String notesDate = dt.toString().split(" ")[0];

                String javaDate = sdf.format(date).split(" ")[0];
                String javaDate2y = sdf2y.format(date).split(" ")[0];

                if (!notesDate.equals(javaDate) && !notesDate.equals(javaDate2y)) {
                    System.out.println("Notes: " + dt);
                    System.out.println("Java: " + date);
                }

                cal.add(Calendar.DAY_OF_MONTH, -1);

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 
On my machine it started to complain about dates starting 29.10.1995. Then the periods were different - sometimes just one month, later whole DST period. 

15.02.2024 10:13:07   Agent Manager: Agent printing: Notes: 29.10.95 00:00:00 CEDT
15.02.2024 10:13:07   Agent Manager: Agent printing: Java: Sat Oct 28 23:00:00 CET 1995
15.02.2024 10:13:07   Agent Manager: Agent printing: Notes: 28.10.95 00:00:00 CEDT
15.02.2024 10:13:07   Agent Manager: Agent printing: Java: Fri Oct 27 23:00:00 CET 1995
15.02.2024 10:13:07   Agent Manager: Agent printing: Notes: 27.10.95 00:00:00 CEDT
15.02.2024 10:13:07   Agent Manager: Agent printing: Java: Thu Oct 26 23:00:00 CET 1995

So the problem is bigger that just a few problematic dates.

Fix routine

The only way how I was able to get the correct date was from DominoDocument.getDocument().getItemValueDateTimeArray(..). But I can't do that in a XPages converter. With some experiments, I have found that I can use different ways to convert the Date back to DateTime and then it can give me a hint that the problem occurred. And then I can adjust the date. 

It turned out that if I create the DateTime from a Calendar instead, I get a different DateTime. Strangely, if I convert that one back to Java Date, I get the wrong date again. But we can get the difference and adjust the original date.

private static Date fixDate(Session s, Date origDate) {
	Date res = origDate;
	try {
		DateTime dateTime = s.createDateTime(origDate);
		Calendar c = Calendar.getInstance();
		c.setTime(origDate);
		DateTime calDateTime = s.createDateTime(c);
		if (!StringUtil.equals(calDateTime.getDateOnly(), dateTime.getDateOnly())) {
			int diff = dateTime.timeDifference(calDateTime);
			c.add(Calendar.SECOND, -diff);
			res = c.getTime();
		}

	} catch (NotesException e) {
		e.printStackTrace();
	}
	return res;
}

Taking this to XPages

Ok, we have a snippet that can save the situation, but injecting it into XPages was harder than expected. The standard converter generates a client-side converter object, which doesn't work with custom converters, so I can't just configure a custom converter on the field (and replacing the converter globally doesn't work as XPages add their one for javax.facesDateTime after processing the app faces config - I learned this the hard way in the past). 

Luckily, Sven Hasselbach blogged about a similar problem for validators - http://hasselba.ch/blog/?p=764. So I can inject the converter via code, so all the instanceof checks then work nicely. 
<xp:this.afterPageLoad>
  <![CDATA[#{javascript:getComponent(compositeData.inputId).setConverter(
	 new com.pradny.domino.converter.CustomDateConverter()
  );}]]>
</xp:this.afterPageLoad>

The converter itself just extends the standard one and uses a pattern defined in the app. It's designed just for this single scenario, so it can't be used for other formats, but it's fine as it's applied only to components in custom controls that use this format.

package comm.pradny.domino.converter;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import com.ibm.commons.util.StringUtil;
import com.ibm.xsp.convert.DateTimeConverter;
import com.ibm.xsp.extlib.util.ExtLibUtil;
import icrc.openLog.OpenLogItem;
import icrc.welcome.Settings;
import lotus.domino.DateTime;
import lotus.domino.NotesException;
import lotus.domino.Session;

public class CustomDateConverter extends DateTimeConverter {
    private static final SimpleDateFormat formatter = new java.text.SimpleDateFormat(Settings.DATE_PATTERN);

    public CustomDateConverter() {
        super();
        setPattern(Settings.DATE_PATTERN);
    }

    private Date fixDate(Session s, Date origDate) {
        Date res = origDate;
        try {
            DateTime dateTime = s.createDateTime(origDate);
            Calendar c = Calendar.getInstance();
            c.setTime(origDate);
            DateTime calDateTime = s.createDateTime(c);
            if (!StringUtil.equals(calDateTime.getDateOnly(), dateTime.getDateOnly())) {
                int diff = dateTime.timeDifference(calDateTime);
                c.add(Calendar.SECOND, -diff);
                res = c.getTime();
            }
        } catch (NotesException e) {
            OpenLogItem.logError(e);
        }
        return res;
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (value instanceof Date) {
            Date origDate = fixDate(ExtLibUtil.getCurrentSession(), (Date) value);
            String s = formatter.format(origDate);
            return s;
        } else {
            throw new IllegalArgumentException("Value is not a java.util.Date");
        }
    }
}

And those few lines seem to get the situation under control. Even if HCL fixes the problem in the API, the code can stay in the app as it does nothing to dates that work already correctly. It doesn't have any hardcoded offset of format, so it can help in other similar situations too. I ran the test agent with the fix routine and no dates were reported, so it should not break other dates. 

Conclusion

The most surprising thing about this problem is that I haven't seen it before. I assume it was always in the API, but I probably never worked intensively with dates before 1995 in an app. In this case, it was detected with date of birth fields. 

I hate time-zone issues and daylight issues even more. The discussion about stopping of EU went completely silent now, but it would be great to get rid of it. Of course, the change would break many systems and cause headaches for many IT people.

I hope that HCL will find a cure at the low API level for this issue as it's not just an XPages issue. Obviously, the proper solution is to get LocalDate implementation in the API and XPages, but the code heavily relies on the old Date class and it would be challenging to replace it everywhere. 

Comments

Popular posts from this blog

HCL Domino 12.0.2, Engage 2022 and HCL Factory tour Milan

 I haven't published my recap after Engage this year and the recent HCL Factory tour in Milan is a great opportunity to write a summary about what's happening in HCL (mostly Domino) space. It's a mix of news about 12.0.2, future directions, and my impressions, so it can be a bit chaotic, but I got the impression that many people see it similarly.  Engage 2022 Engage 2022 was great (as always). I love the atmosphere in Brudges. I visited it once after Engage a few years ago and I was happy to come back. This was also the first time I had the opportunity to speak at Engage, which obviously made it a bit more stressful, but also more fun. Together with Domino Jams, HCL continued conversations with customers and partners about the future of their products at Engage. Many of these ideas were now discussed in greater detail in Milan, some of them were even demoed.  My main takeaways from Engage were: Nomad (web and mobile) are a great addition to Notes family Restyle is a great...

XPages EL/class-loader memory leak (now with solution)

 We have recently experienced OutOfMemory crashes of XPages app server. The server was recently upgraded to 12.0.1FP1, but we were getting some panic crashes in HTTP even before the upgrade (it was 9.0.1FP10). Our hopes were that the upgrade would stabilize the server, but it's not the case. At least now I start to see what's the problem.  update 8.12.2022 There were actually 3 different leaks. I have rewritten the article to be a bit more clear. I also re-run some of the tests on 9.0.1FP10, so I assume the problems are also in earlier versions. Problem 1 The server is hosting over 1000 NSF sharing the same design + some other custom apps. Not all NSFs are used via web as the app still has classic Notes UI in parallel, so it's a bit tricky to estimate the load. By using tell http xsp show modules I usually see around 350 NSFs active. We kept the default application timeout that should provide reasonable application recycling if it's not used continuously.  We started to...