|
|
@@ -23,10 +23,10 @@ import java.time.temporal.Temporal; |
|
|
|
import java.time.temporal.TemporalAdjuster; |
|
|
|
import java.util.Calendar; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.EventObject; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Locale; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Objects; |
|
|
|
import java.util.Optional; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.logging.Logger; |
|
|
@@ -91,7 +91,7 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
|
|
|
|
private boolean lenient = false; |
|
|
|
|
|
|
|
private String dateString = null; |
|
|
|
private String dateString = ""; |
|
|
|
|
|
|
|
private String currentParseErrorMessage; |
|
|
|
|
|
|
@@ -110,13 +110,6 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
|
|
|
|
private String dateOutOfRangeMessage = "Date is out of allowed range"; |
|
|
|
|
|
|
|
/** |
|
|
|
* Determines whether the ValueChangeEvent should be fired. Used to prevent |
|
|
|
* firing the event when UI has invalid string until uiHasValidDateString |
|
|
|
* flag is set |
|
|
|
*/ |
|
|
|
private boolean preventValueChangeEvent; |
|
|
|
|
|
|
|
/* Constructors */ |
|
|
|
|
|
|
|
/** |
|
|
@@ -185,11 +178,6 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
target.addAttribute(DateFieldConstants.ATTR_WEEK_NUMBERS, |
|
|
|
isShowISOWeekNumbers()); |
|
|
|
target.addAttribute("parsable", uiHasValidDateString); |
|
|
|
/* |
|
|
|
* TODO communicate back the invalid date string? E.g. returning back to |
|
|
|
* app or refresh. |
|
|
|
*/ |
|
|
|
|
|
|
|
final T currentDate = getValue(); |
|
|
|
|
|
|
|
// Only paint variables for the resolution and up, e.g. Resolution DAY |
|
|
@@ -218,116 +206,59 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
|
|
|
|
// Old and new dates |
|
|
|
final T oldDate = getValue(); |
|
|
|
T newDate = null; |
|
|
|
|
|
|
|
// this enables analyzing invalid input on the server |
|
|
|
// this variable is null if the date was chosen with popup calendar |
|
|
|
// or contains user-typed string |
|
|
|
final String newDateString = (String) variables.get("dateString"); |
|
|
|
dateString = newDateString; |
|
|
|
|
|
|
|
// Gets the new date in parts |
|
|
|
T newDate; |
|
|
|
|
|
|
|
boolean hasChanges = false; |
|
|
|
Map<R, Integer> calendarFields = new HashMap<>(); |
|
|
|
|
|
|
|
for (R resolution : getResolutionsHigherOrEqualTo( |
|
|
|
getResolution())) { |
|
|
|
// Only handle what the client is allowed to send. The same |
|
|
|
// resolutions that are painted |
|
|
|
String variableName = getResolutionVariable(resolution); |
|
|
|
|
|
|
|
int value = getDatePart(oldDate, resolution); |
|
|
|
if (variables.containsKey(variableName)) { |
|
|
|
Integer newValue = (Integer) variables.get(variableName); |
|
|
|
if (newValue >= 0) { |
|
|
|
hasChanges = true; |
|
|
|
value = newValue; |
|
|
|
} |
|
|
|
} |
|
|
|
calendarFields.put(resolution, value); |
|
|
|
} |
|
|
|
|
|
|
|
// If no new variable values were received, use the previous value |
|
|
|
if (!hasChanges) { |
|
|
|
if ("".equals(newDateString)) { |
|
|
|
|
|
|
|
newDate = null; |
|
|
|
// TODO check if the following 3 lines are necessary |
|
|
|
hasChanges = !uiHasValidDateString; |
|
|
|
uiHasValidDateString = true; |
|
|
|
currentParseErrorMessage = null; |
|
|
|
} else { |
|
|
|
newDate = buildDate(calendarFields); |
|
|
|
newDate = reconstructDateFromFields(variables, oldDate); |
|
|
|
} |
|
|
|
|
|
|
|
if (newDate == null && dateString != null |
|
|
|
&& !dateString.isEmpty()) { |
|
|
|
Result<T> parsedDate = handleUnparsableDateString(dateString); |
|
|
|
if (parsedDate.isError()) { |
|
|
|
|
|
|
|
/* |
|
|
|
* Saves the localized message of parse error. This can be |
|
|
|
* overridden in handleUnparsableDateString. The message |
|
|
|
* will later be used to show a validation error. |
|
|
|
*/ |
|
|
|
currentParseErrorMessage = parsedDate.getMessage().get(); |
|
|
|
|
|
|
|
/* |
|
|
|
* The value of the DateField should be null if an invalid |
|
|
|
* value has been given. Not using setValue() since we do |
|
|
|
* not want to cause the client side value to change. |
|
|
|
*/ |
|
|
|
uiHasValidDateString = false; |
|
|
|
|
|
|
|
/* |
|
|
|
* Datefield now contains some text that could't be parsed |
|
|
|
* into date. ValueChangeEvent is fired after the value is |
|
|
|
* changed and the flags are set |
|
|
|
*/ |
|
|
|
if (oldDate != null) { |
|
|
|
/* |
|
|
|
* Set the logic value to null without firing the |
|
|
|
* ValueChangeEvent |
|
|
|
*/ |
|
|
|
preventValueChangeEvent = true; |
|
|
|
try { |
|
|
|
setValue(null); |
|
|
|
} finally { |
|
|
|
preventValueChangeEvent = false; |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Reset the dateString (overridden to null by setValue) |
|
|
|
*/ |
|
|
|
dateString = newDateString; |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* If value was changed fire the ValueChangeEvent |
|
|
|
*/ |
|
|
|
if (oldDate != null) { |
|
|
|
fireEvent(createValueChange(oldDate, true)); |
|
|
|
} |
|
|
|
hasChanges |= !Objects.equals(dateString, newDateString) || |
|
|
|
!Objects.equals(oldDate, newDate); |
|
|
|
|
|
|
|
markAsDirty(); |
|
|
|
if (hasChanges) { |
|
|
|
dateString = newDateString; |
|
|
|
if (newDateString == null || newDateString.isEmpty()) { |
|
|
|
uiHasValidDateString = true; |
|
|
|
currentParseErrorMessage = null; |
|
|
|
setValue(newDate, true); |
|
|
|
setComponentError(null); |
|
|
|
} else { |
|
|
|
parsedDate.ifOk(value -> { |
|
|
|
if (variables.get("lastInvalidDateString") != null) { |
|
|
|
Result<T> parsedDate = handleUnparsableDateString(dateString); |
|
|
|
parsedDate.ifOk(v-> { |
|
|
|
uiHasValidDateString = true; |
|
|
|
currentParseErrorMessage = null; |
|
|
|
setValue(v,true); |
|
|
|
}); |
|
|
|
if (parsedDate.isError()) { |
|
|
|
dateString = null; |
|
|
|
uiHasValidDateString = false; |
|
|
|
currentParseErrorMessage = parsedDate.getMessage().orElse("Parsing error"); |
|
|
|
setComponentError(new UserError(getParseErrorMessage())); |
|
|
|
setValue(null,true); |
|
|
|
} |
|
|
|
} else { |
|
|
|
uiHasValidDateString = true; |
|
|
|
currentParseErrorMessage = null; |
|
|
|
setValue(value, true); |
|
|
|
}); |
|
|
|
|
|
|
|
/* |
|
|
|
* Ensure the value is sent to the client if the value is |
|
|
|
* set to the same as the previous (#4304). Does not repaint |
|
|
|
* if handleUnparsableDateString throws an exception. In |
|
|
|
* this case the invalid text remains in the DateField. |
|
|
|
*/ |
|
|
|
markAsDirty(); |
|
|
|
setValue(newDate, true); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} else if (newDate != oldDate |
|
|
|
&& (newDate == null || !newDate.equals(oldDate))) { |
|
|
|
currentParseErrorMessage = null; |
|
|
|
setValue(newDate, true); // Don't require a repaint, client |
|
|
|
// updates itself |
|
|
|
} else if (!uiHasValidDateString) { |
|
|
|
// oldDate == |
|
|
|
// newDate == null |
|
|
|
// Empty value set, previously contained unparsable date string, |
|
|
|
// clear related internal fields |
|
|
|
setValue(null); |
|
|
|
markAsDirty(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@@ -340,6 +271,31 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Construct a date object from the individual field values received from the |
|
|
|
* client. |
|
|
|
* |
|
|
|
* @since 8.1.1 |
|
|
|
*/ |
|
|
|
protected T reconstructDateFromFields(Map<String, Object> variables, T oldDate) { |
|
|
|
Map<R, Integer> calendarFields = new HashMap<>(); |
|
|
|
|
|
|
|
for (R resolution : getResolutionsHigherOrEqualTo( |
|
|
|
getResolution())) { |
|
|
|
// Only handle what the client is allowed to send. The same |
|
|
|
// resolutions that are painted |
|
|
|
String variableName = getResolutionVariable(resolution); |
|
|
|
|
|
|
|
Integer newValue = (Integer) variables.get(variableName); |
|
|
|
if (newValue != null && newValue >= 0) { |
|
|
|
calendarFields.put(resolution, newValue); |
|
|
|
} else { |
|
|
|
calendarFields.put(resolution, getDatePart(oldDate, resolution)); |
|
|
|
} |
|
|
|
} |
|
|
|
return buildDate(calendarFields); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Sets the start range for this component. If the value is set before this |
|
|
|
* date (taking the resolution into account), the component will not |
|
|
@@ -503,34 +459,6 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
return value; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Sets the value of this object. If the new value is not equal to |
|
|
|
* {@code getValue()}, fires a {@link ValueChangeEvent} . |
|
|
|
* |
|
|
|
* @param value |
|
|
|
* the new value, may be {@code null} |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
public void setValue(T value) { |
|
|
|
/* |
|
|
|
* First handle special case when the client side component have a date |
|
|
|
* string but value is null (e.g. unparsable date string typed in by the |
|
|
|
* user). No value changes should happen, but we need to do some |
|
|
|
* internal housekeeping. |
|
|
|
*/ |
|
|
|
if (value == null && !uiHasValidDateString) { |
|
|
|
/* |
|
|
|
* Side-effects of doSetValue clears possible previous strings and |
|
|
|
* flags about invalid input. |
|
|
|
*/ |
|
|
|
doSetValue(null); |
|
|
|
|
|
|
|
markAsDirty(); |
|
|
|
return; |
|
|
|
} |
|
|
|
super.setValue(value); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Checks whether ISO 8601 week numbers are shown in the date selector. |
|
|
|
* |
|
|
@@ -617,11 +545,23 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
} else { |
|
|
|
throw new RuntimeException("Cannot detect resoluton type " |
|
|
|
+ Optional.ofNullable(dateType).map(Type::getTypeName) |
|
|
|
.orElse(null)); |
|
|
|
.orElse(null)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Formats date according to the components locale. |
|
|
|
* To be reimplemented in subclasses. |
|
|
|
* |
|
|
|
* @param value the date or {@code null} |
|
|
|
* @return textual representation of the date or empty string for {@code null} |
|
|
|
* @since 8.1.1 |
|
|
|
*/ |
|
|
|
protected String formatDate(T value) { |
|
|
|
return Objects.toString(value, ""); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public void writeDesign(Element design, DesignContext designContext) { |
|
|
|
super.writeDesign(design, designContext); |
|
|
@@ -631,17 +571,6 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
protected void fireEvent(EventObject event) { |
|
|
|
if (event instanceof ValueChangeEvent) { |
|
|
|
if (!preventValueChangeEvent) { |
|
|
|
super.fireEvent(event); |
|
|
|
} |
|
|
|
} else { |
|
|
|
super.fireEvent(event); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* This method is called to handle a non-empty date string from the client |
|
|
|
* if the client could not parse it as a Date. |
|
|
@@ -673,26 +602,24 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & |
|
|
|
|
|
|
|
@Override |
|
|
|
protected void doSetValue(T value) { |
|
|
|
|
|
|
|
this.value = value; |
|
|
|
// Also set the internal dateString |
|
|
|
if (value != null) { |
|
|
|
dateString = value.toString(); |
|
|
|
dateString = formatDate(value); |
|
|
|
} else { |
|
|
|
dateString = null; |
|
|
|
dateString = formatDate(getEmptyValue()); |
|
|
|
} |
|
|
|
|
|
|
|
this.value = value; |
|
|
|
setComponentError(null); |
|
|
|
if (!uiHasValidDateString) { |
|
|
|
// clear component error and parsing flag |
|
|
|
uiHasValidDateString = true; |
|
|
|
setComponentError(new UserError(currentParseErrorMessage)); |
|
|
|
RangeValidator<T> validator = getRangeValidator();// TODO move range check to internal validator? |
|
|
|
ValidationResult result = validator.apply(value, |
|
|
|
new ValueContext(this, this)); |
|
|
|
if (result.isError()) { |
|
|
|
currentParseErrorMessage = getDateOutOfRangeMessage(); |
|
|
|
} |
|
|
|
if (currentParseErrorMessage == null) { |
|
|
|
setComponentError(null); |
|
|
|
} else { |
|
|
|
RangeValidator<T> validator = getRangeValidator(); |
|
|
|
ValidationResult result = validator.apply(value, |
|
|
|
new ValueContext(this, this)); |
|
|
|
if (result.isError()) { |
|
|
|
setComponentError(new UserError(getDateOutOfRangeMessage())); |
|
|
|
} |
|
|
|
setComponentError(new UserError(currentParseErrorMessage)); |
|
|
|
} |
|
|
|
} |
|
|
|
|