Skip to content

Commit df6912d

Browse files
committed
MID-11078 Fix false dateTime updates due to seconds normalization in admin GUI
1 parent 1f8f55d commit df6912d

File tree

3 files changed

+310
-6
lines changed

3 files changed

+310
-6
lines changed

gui/admin-gui/src/main/java/com/evolveum/midpoint/web/model/XmlGregorianCalendarModel.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,29 +44,29 @@ public void setObject(Date object) {
4444

4545
XMLGregorianCalendar newValue = MiscUtil.asXMLGregorianCalendar(object);
4646

47-
GregorianCalendar current = cloneAndStripSeconds(model.getObject());
48-
if (current != null) {
47+
Long currentMinuteMillis = getNormalizedMinuteEpochMillis(model.getObject());
48+
if (currentMinuteMillis != null) {
4949
// this check is done on UI side to prevent stripping of seconds and milliseconds when date was not changed
5050
// This happens because of the way how date picker works - it doesn't have seconds/miliseconds field therefore
5151
// those fields submitted via html form are always zeroed.
5252
// See MID-9733 for more info.
53-
GregorianCalendar newCal = cloneAndStripSeconds(newValue);
54-
if (current.equals(newCal)) {
53+
Long newMinuteMillis = getNormalizedMinuteEpochMillis(newValue);
54+
if (currentMinuteMillis.equals(newMinuteMillis)) {
5555
return;
5656
}
5757
}
5858

5959
model.setObject(newValue);
6060
}
6161

62-
private GregorianCalendar cloneAndStripSeconds(XMLGregorianCalendar cal) {
62+
private Long getNormalizedMinuteEpochMillis(XMLGregorianCalendar cal) {
6363
if (cal == null) {
6464
return null;
6565
}
6666
GregorianCalendar c = cal.toGregorianCalendar();
6767
c.set(Calendar.SECOND, 0);
6868
c.set(Calendar.MILLISECOND, 0);
6969

70-
return c;
70+
return c.getTimeInMillis();
7171
}
7272
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*
2+
* Copyright (C) 2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*/
6+
7+
package com.evolveum.midpoint.web;
8+
9+
import static org.testng.Assert.assertNotEquals;
10+
import static org.testng.Assert.assertSame;
11+
import static org.testng.AssertJUnit.assertEquals;
12+
13+
import java.math.BigDecimal;
14+
import java.math.BigInteger;
15+
import java.util.Calendar;
16+
import java.util.Date;
17+
import java.util.GregorianCalendar;
18+
import java.util.Locale;
19+
import java.util.TimeZone;
20+
import javax.xml.datatype.DatatypeFactory;
21+
import javax.xml.datatype.Duration;
22+
import javax.xml.datatype.XMLGregorianCalendar;
23+
import javax.xml.namespace.QName;
24+
25+
import com.evolveum.midpoint.web.model.XmlGregorianCalendarModel;
26+
27+
import org.apache.wicket.model.IModel;
28+
import org.testng.annotations.AfterMethod;
29+
import org.testng.annotations.Test;
30+
31+
public class TestXmlGregorianCalendarModel extends AbstractGuiUnitTest {
32+
33+
private static final DatatypeFactory DF = datatypeFactory();
34+
35+
private final TimeZone originalDefaultTimeZone = TimeZone.getDefault();
36+
37+
@AfterMethod
38+
void restoreDefaultTimeZone() {
39+
TimeZone.setDefault(originalDefaultTimeZone);
40+
}
41+
42+
@Test
43+
void testKeepStoredValueWhenGuiOnlyDropsSeconds() {
44+
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague"));
45+
46+
XMLGregorianCalendar stored = DF.newXMLGregorianCalendar("2025-05-15T09:32:12+02:00");
47+
GregorianCalendar sameMinuteDifferentCalendar = stored.toGregorianCalendar();
48+
sameMinuteDifferentCalendar.setLenient(false);
49+
XMLGregorianCalendar wrapped = new CalendarOverride(stored, sameMinuteDifferentCalendar);
50+
51+
IModel<XMLGregorianCalendar> backing = new SimpleModel(wrapped);
52+
XmlGregorianCalendarModel model = new XmlGregorianCalendarModel(backing);
53+
54+
Date submitted = minuteDate(32);
55+
56+
GregorianCalendar current = wrapped.toGregorianCalendar();
57+
current.set(Calendar.SECOND, 0);
58+
current.set(Calendar.MILLISECOND, 0);
59+
60+
GregorianCalendar submittedCalendar = storedFromDate(submitted).toGregorianCalendar();
61+
submittedCalendar.set(Calendar.SECOND, 0);
62+
submittedCalendar.set(Calendar.MILLISECOND, 0);
63+
64+
assertEquals(current.getTimeInMillis(), submittedCalendar.getTimeInMillis());
65+
assertNotEquals(current, submittedCalendar);
66+
67+
model.setObject(submitted);
68+
69+
assertSame(wrapped, backing.getObject());
70+
assertEquals(12, backing.getObject().getSecond());
71+
assertEquals("2025-05-15T09:32:12+02:00", backing.getObject().toXMLFormat());
72+
}
73+
74+
@Test
75+
void testReplaceValueWhenMinuteChanges() {
76+
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague"));
77+
78+
XMLGregorianCalendar stored = DF.newXMLGregorianCalendar("2025-05-15T09:32:12+02:00");
79+
IModel<XMLGregorianCalendar> backing = new SimpleModel(stored);
80+
XmlGregorianCalendarModel model = new XmlGregorianCalendarModel(backing);
81+
82+
model.setObject(minuteDate(33));
83+
84+
assertEquals("2025-05-15T09:33:00.000+02:00", backing.getObject().toXMLFormat());
85+
}
86+
87+
private static Date minuteDate(int minute) {
88+
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("Europe/Prague"));
89+
calendar.clear();
90+
calendar.set(2025, Calendar.MAY, 15, 9, minute, 0);
91+
calendar.set(Calendar.MILLISECOND, 0);
92+
return calendar.getTime();
93+
}
94+
95+
private static XMLGregorianCalendar storedFromDate(Date date) {
96+
GregorianCalendar calendar = new GregorianCalendar();
97+
calendar.setTime(date);
98+
return DF.newXMLGregorianCalendar(calendar);
99+
}
100+
101+
private static DatatypeFactory datatypeFactory() {
102+
try {
103+
return DatatypeFactory.newInstance();
104+
} catch (Exception e) {
105+
throw new IllegalStateException(e);
106+
}
107+
}
108+
109+
private static final class SimpleModel implements IModel<XMLGregorianCalendar> {
110+
private XMLGregorianCalendar object;
111+
112+
private SimpleModel(XMLGregorianCalendar object) {
113+
this.object = object;
114+
}
115+
116+
@Override
117+
public XMLGregorianCalendar getObject() {
118+
return object;
119+
}
120+
121+
@Override
122+
public void setObject(XMLGregorianCalendar object) {
123+
this.object = object;
124+
}
125+
}
126+
127+
// Forces toGregorianCalendar() to return a calendar configuration that reproduces
128+
// the old GregorianCalendar.equals(...) mismatch for the same normalized minute.
129+
private static final class CalendarOverride extends XMLGregorianCalendar {
130+
private final XMLGregorianCalendar delegate;
131+
private final GregorianCalendar overriddenCalendar;
132+
133+
private CalendarOverride(XMLGregorianCalendar delegate, GregorianCalendar overriddenCalendar) {
134+
this.delegate = delegate;
135+
this.overriddenCalendar = overriddenCalendar;
136+
}
137+
138+
@Override
139+
public GregorianCalendar toGregorianCalendar() {
140+
return (GregorianCalendar) overriddenCalendar.clone();
141+
}
142+
143+
@Override
144+
public Object clone() {
145+
return new CalendarOverride((XMLGregorianCalendar) delegate.clone(), (GregorianCalendar) overriddenCalendar.clone());
146+
}
147+
148+
@Override
149+
public void clear() {
150+
delegate.clear();
151+
}
152+
153+
@Override
154+
public void reset() {
155+
delegate.reset();
156+
}
157+
158+
@Override
159+
public void setYear(BigInteger year) {
160+
delegate.setYear(year);
161+
}
162+
163+
@Override
164+
public void setYear(int year) {
165+
delegate.setYear(year);
166+
}
167+
168+
@Override
169+
public void setMonth(int month) {
170+
delegate.setMonth(month);
171+
}
172+
173+
@Override
174+
public void setDay(int day) {
175+
delegate.setDay(day);
176+
}
177+
178+
@Override
179+
public void setTimezone(int offset) {
180+
delegate.setTimezone(offset);
181+
}
182+
183+
@Override
184+
public void setHour(int hour) {
185+
delegate.setHour(hour);
186+
}
187+
188+
@Override
189+
public void setMinute(int minute) {
190+
delegate.setMinute(minute);
191+
}
192+
193+
@Override
194+
public void setSecond(int second) {
195+
delegate.setSecond(second);
196+
}
197+
198+
@Override
199+
public void setMillisecond(int millisecond) {
200+
delegate.setMillisecond(millisecond);
201+
}
202+
203+
@Override
204+
public void setFractionalSecond(BigDecimal fractional) {
205+
delegate.setFractionalSecond(fractional);
206+
}
207+
208+
@Override
209+
public BigInteger getEon() {
210+
return delegate.getEon();
211+
}
212+
213+
@Override
214+
public int getYear() {
215+
return delegate.getYear();
216+
}
217+
218+
@Override
219+
public BigInteger getEonAndYear() {
220+
return delegate.getEonAndYear();
221+
}
222+
223+
@Override
224+
public int getMonth() {
225+
return delegate.getMonth();
226+
}
227+
228+
@Override
229+
public int getDay() {
230+
return delegate.getDay();
231+
}
232+
233+
@Override
234+
public int getTimezone() {
235+
return delegate.getTimezone();
236+
}
237+
238+
@Override
239+
public int getHour() {
240+
return delegate.getHour();
241+
}
242+
243+
@Override
244+
public int getMinute() {
245+
return delegate.getMinute();
246+
}
247+
248+
@Override
249+
public int getSecond() {
250+
return delegate.getSecond();
251+
}
252+
253+
@Override
254+
public BigDecimal getFractionalSecond() {
255+
return delegate.getFractionalSecond();
256+
}
257+
258+
@Override
259+
public int compare(XMLGregorianCalendar xmlGregorianCalendar) {
260+
return delegate.compare(xmlGregorianCalendar);
261+
}
262+
263+
@Override
264+
public XMLGregorianCalendar normalize() {
265+
return delegate.normalize();
266+
}
267+
268+
@Override
269+
public String toXMLFormat() {
270+
return delegate.toXMLFormat();
271+
}
272+
273+
@Override
274+
public QName getXMLSchemaType() {
275+
return delegate.getXMLSchemaType();
276+
}
277+
278+
@Override
279+
public boolean isValid() {
280+
return delegate.isValid();
281+
}
282+
283+
@Override
284+
public void add(Duration duration) {
285+
delegate.add(duration);
286+
}
287+
288+
@Override
289+
public GregorianCalendar toGregorianCalendar(TimeZone timezone, Locale locale, XMLGregorianCalendar defaults) {
290+
return delegate.toGregorianCalendar(timezone, locale, defaults);
291+
}
292+
293+
@Override
294+
public TimeZone getTimeZone(int defaultZoneoffset) {
295+
return delegate.getTimeZone(defaultZoneoffset);
296+
}
297+
298+
@Override
299+
public int getMillisecond() {
300+
return delegate.getMillisecond();
301+
}
302+
}
303+
}

release-notes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ Overall, midPoint 4.10 opens up the world of identity management and governance
9494
* Fixed missing estimatedOldValues in raw audit (EXECUTE_CHANGES_RAW). See bug:MID-10027[]
9595
* Fixed modification of query of report fails when the query is defined using midPoint query language. See bug:MID-11093[]
9696
* Fixed access request shows unrelated conflicts. See bug:MID-10994[]
97+
* Fixed false replace updates in admin GUI dateTime comparison (seconds normalization issue). See bug:MID-11078[]
9798

9899
=== Releases Of Other Components
99100

0 commit comments

Comments
 (0)