zul/src/main/java/org/zkoss/zul/Datebox.java
/* Datebox.java
Purpose:
Description:
History:
Tue Jun 28 13:41:01 2005, Created by tomyeh
Copyright (C) 2005 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.chrono.Chronology;
import java.time.chrono.Era;
import java.time.chrono.IsoEra;
import java.time.chrono.JapaneseEra;
import java.time.chrono.MinguoDate;
import java.time.chrono.MinguoEra;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.TextStyle;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.mesg.Messages;
import org.zkoss.text.DateFormats;
import org.zkoss.util.Locales;
import org.zkoss.util.TimeZones;
import org.zkoss.util.WaitLock;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Blockable;
import org.zkoss.zk.ui.http.Utils;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zul.impl.DateTimeFormatInputElement;
import org.zkoss.zul.impl.FormatInputElement;
import org.zkoss.zul.impl.XulElement;
import org.zkoss.zul.mesg.MZul;
/**
* An edit box for holding a date.
*
* <p>
* The default format ({@link #getFormat}) depends on {@link DateFormats#getDateFormat(int, Locale, String)}
* and the current user's locale (unless {@link #setLocale} is assigned.
* Please refer to {@link #setFormat} for more details.
* <p>
* Default {@link #getZclass}: z-datebox.(since 3.5.0)
*
* @author tomyeh
*/
public class Datebox extends DateTimeFormatInputElement {
private static final Logger log = LoggerFactory.getLogger(Datebox.class);
public static final String DEFAULT_FORMAT = "yyyy/MM/dd";
private List<TimeZone> _dtzones;
private boolean _btnVisible = true, _lenient = true, _dtzonesReadonly = false;
private static Map<Locale, Object> _symbols = new HashMap<Locale, Object>(8);
private boolean _weekOfYear;
private boolean _showTodayLink = false;
private boolean _strictDate = false;
private String _position = "after_start";
private String _todayLinkLabel = Messages.get(MZul.CALENDAR_TODAY);
private LocalDateTime _defaultDateTime;
private String _selectLevel = "day";
private boolean _closePopupOnTimezoneChange = true;
static {
addClientEvent(Datebox.class, Events.ON_TIME_ZONE_CHANGE, CE_IMPORTANT | CE_DUPLICATE_IGNORE);
}
public Datebox() {
setFormat("");
setCols(11);
}
/** Constructor with a given date.
* @param date the date to be assigned to this datebox initially.<br/>
* Notice that, if this datebox does not allow users to select the time
* (i.e., the format limited to year, month and day), the date specified here
* is better to set hour, minutes, seconds and milliseconds to zero
* (for the current timezone, {@link TimeZones#getCurrent}), so it is easier
* to work with other libraries, such as SQL.
* {@link org.zkoss.util.Dates} has a set of utilities to simplify the task.
*/
public Datebox(Date date) throws WrongValueException {
this();
setValue(date);
}
/** Constructor with a given date.
* @param value the date to be assigned to this datebox initially.<br/>
* Notice that, if this datebox does not allow users to select the time
* (i.e., the format limited to year, month and day), the date specified here
* is better to set hour, minutes, seconds and milliseconds to zero
* (for the current timezone, {@link TimeZones#getCurrent}), so it is easier
* to work with other libraries, such as SQL.
* {@link org.zkoss.util.Dates} has a set of utilities to simplify the task.
* @since 9.0.0
*/
public Datebox(ZonedDateTime value) throws WrongValueException {
this();
setValueInZonedDateTime(value);
}
/** Constructor with a given date.
* @param value the date to be assigned to this datebox initially.<br/>
* Notice that, if this datebox does not allow users to select the time
* (i.e., the format limited to year, month and day), the date specified here
* is better to set hour, minutes, seconds and milliseconds to zero
* (for the current timezone, {@link TimeZones#getCurrent}), so it is easier
* to work with other libraries, such as SQL.
* {@link org.zkoss.util.Dates} has a set of utilities to simplify the task.
* @since 9.0.0
*/
public Datebox(LocalDateTime value) throws WrongValueException {
this();
setValueInLocalDateTime(value);
}
/** Constructor with a given date.
* @param value the date to be assigned to this datebox initially.<br/>
* Notice that, if this datebox does not allow users to select the time
* (i.e., the format limited to year, month and day), the date specified here
* is better to set hour, minutes, seconds and milliseconds to zero
* (for the current timezone, {@link TimeZones#getCurrent}), so it is easier
* to work with other libraries, such as SQL.
* {@link org.zkoss.util.Dates} has a set of utilities to simplify the task.
* @since 9.0.0
*/
public Datebox(LocalDate value) throws WrongValueException {
this();
setValueInLocalDate(value);
}
/** Constructor with a given date.
* @param value the date to be assigned to this datebox initially.<br/>
* Notice that, if this datebox does not allow users to select the time
* (i.e., the format limited to year, month and day), the date specified here
* is better to set hour, minutes, seconds and milliseconds to zero
* (for the current timezone, {@link TimeZones#getCurrent}), so it is easier
* to work with other libraries, such as SQL.
* {@link org.zkoss.util.Dates} has a set of utilities to simplify the task.
* @since 9.0.0
*/
public Datebox(LocalTime value) throws WrongValueException {
this();
setValueInLocalTime(value);
}
/**
* Sets whether enable to show the week number in the current calendar or
* not.
* [ZK EE]
* @since 6.5.0
*/
public void setWeekOfYear(boolean weekOfYear) {
if (_weekOfYear != weekOfYear) {
_weekOfYear = weekOfYear;
smartUpdate("weekOfYear", _weekOfYear);
}
}
/**
* Returns whether enable to show the week number in the current calendar or not.
* <p>Default: false
* @since 6.5.0
*/
public boolean isWeekOfYear() {
return _weekOfYear;
}
/**
* Sets whether or not date/time should be strict.
* If true, any invalid input like "Jan 0" or "Nov 31" would be refused.
* If false, it won't be checked and let lenient parsing decide.
*
* @since 8.6.0
*/
public void setStrictDate(boolean strictDate) {
if (_strictDate != strictDate) {
_strictDate = strictDate;
smartUpdate("strictDate", strictDate);
}
}
/**
* Returns whether date/time should be strict or not.
* <p>Default: false.
*
* @since 8.6.0
*/
public boolean isStrictDate() {
return _strictDate;
}
/**
* Returns the default format, which is used when constructing a datebox,
* or when {@link #setFormat} is called with null or empty.
* <p>Default: DateFormats.getDateFormat(DEFAULT, null, "yyyy/MM/dd")
* (see {@link DateFormats#getDateFormat}).
*
* <p>Though you might override this method to provide your own default format,
* it is suggested to specify the format for the current thread
* with {@link DateFormats#setLocalFormatInfo}.
*/
protected String getDefaultFormat() {
return DateFormats.getDateFormat(DateFormat.DEFAULT, _locale, DEFAULT_FORMAT);
//We use yyyy/MM/dd for backward compatibility
}
/**
* Returns the localized format, which is used when constructing a datebox.
* <p>
* You might override this method to provide your own localized format.
*/
protected String getLocalizedFormat() {
return new SimpleDateFormat(getRealFormat(), _locale != null ? _locale : Locales.getCurrent())
.toLocalizedPattern();
}
/**
* Returns whether date/time parsing is to be lenient or not.
*
* <p>
* With lenient parsing, the parser may use heuristics to interpret inputs
* that do not precisely match this object's format. With strict parsing,
* inputs must match this object's format.
*/
public boolean isLenient() {
return _lenient;
}
/**
* Sets whether date/time parsing is to be lenient or not.
* <p>
* Default: true.
*
* <p>
* With lenient parsing, the parser may use heuristics to interpret inputs
* that do not precisely match this object's format. With strict parsing,
* inputs must match this object's format.
*/
public void setLenient(boolean lenient) {
if (_lenient != lenient) {
_lenient = lenient;
smartUpdate("lenient", _lenient);
}
}
/**
* Returns whether the button (on the right of the textbox) is visible.
* <p>
* Default: true.
*
* @since 2.4.1
*/
public boolean isButtonVisible() {
return _btnVisible;
}
/**
* Sets whether the button (on the right of the textbox) is visible.
*
* @since 2.4.1
*/
public void setButtonVisible(boolean visible) {
if (_btnVisible != visible) {
_btnVisible = visible;
smartUpdate("buttonVisible", visible);
}
}
/**
* @return the datebox popup position
* @since 8.0.3
*/
public String getPosition() {
return _position;
}
/**
* Position the popup datebox to the specified location.
* @param position where to position. Default: <code>after_start</code>
* Allowed values:</br>
* <ul>
* <li><b>before_start</b><br/> the element appears above the anchor, aligned to the left.</li>
* <li><b>before_center</b><br/> the element appears above the anchor, aligned to the center.</li>
* <li><b>before_end</b><br/> the element appears above the anchor, aligned to the right.</li>
* <li><b>after_start</b><br/> the element appears below the anchor, aligned to the left.</li>
* <li><b>after_center</b><br/> the element appears below the anchor, aligned to the center.</li>
* <li><b>after_end</b><br/> the element appears below the anchor, aligned to the right.</li>
* <li><b>start_before</b><br/> the element appears to the left of the anchor, aligned to the top.</li>
* <li><b>start_center</b><br/> the element appears to the left of the anchor, aligned to the middle.</li>
* <li><b>start_after</b><br/> the element appears to the left of the anchor, aligned to the bottom.</li>
* <li><b>end_before</b><br/> the element appears to the right of the anchor, aligned to the top.</li>
* <li><b>end_center</b><br/> the element appears to the right of the anchor, aligned to the middle.</li>
* <li><b>end_after</b><br/> the element appears to the right of the anchor, aligned to the bottom.</li>
* <li><b>overlap/top_left</b><br/> the element overlaps the anchor, with anchor and element aligned at top-left.</li>
* <li><b>top_center</b><br/> the element overlaps the anchor, with anchor and element aligned at top-center.</li>
* <li><b>overlap_end/top_right</b><br/> the element overlaps the anchor, with anchor and element aligned at top-right.</li>
* <li><b>middle_left</b><br/> the element overlaps the anchor, with anchor and element aligned at middle-left.</li>
* <li><b>middle_center</b><br/> the element overlaps the anchor, with anchor and element aligned at middle-center.</li>
* <li><b>middle_right</b><br/> the element overlaps the anchor, with anchor and element aligned at middle-right.</li>
* <li><b>overlap_before/bottom_left</b><br/> the element overlaps the anchor, with anchor and element aligned at bottom-left.</li>
* <li><b>bottom_center</b><br/> the element overlaps the anchor, with anchor and element aligned at bottom-center.</li>
* <li><b>overlap_after/bottom_right</b><br/> the element overlaps the anchor, with anchor and element aligned at bottom-right.</li>
* <li><b>at_pointer</b><br/> the element appears with the upper-left aligned with the mouse cursor.</li>
* <li><b>after_pointer</b><br/> the element appears with the top aligned with
* the bottom of the mouse cursor, with the left side of the element at the horizontal position of the mouse cursor.</li>
* </ul>
* @since 8.0.3
*/
public void setPosition(String position) {
if (!Objects.equals(_position, position)) {
_position = position;
smartUpdate("position", _position);
}
}
/** Sets the date format.
<p>If null or empty is specified, {@link #getDefaultFormat} is assumed.
Since 5.0.7, you could specify one of the following reserved words,
and {@link DateFormats#getDateFormat} or {@link DateFormats#getDateTimeFormat}
will be used to retrieve the real format.
<table border=0 cellspacing=3 cellpadding=0>
<tr>
<td>short</td>
<td>{@link DateFormats#getDateFormat} with {@link DateFormat#SHORT}</td>
</tr>
<tr>
<td>medium</td>
<td>{@link DateFormats#getDateFormat} with {@link DateFormat#MEDIUM}</td>
</tr>
<tr>
<td>long</td>
<td>{@link DateFormats#getDateFormat} with {@link DateFormat#LONG}</td>
</tr>
<tr>
<td>full</td>
<td>{@link DateFormats#getDateFormat} with {@link DateFormat#FULL}</td>
</tr>
</table>
<p>To specify a date/time format, you could specify two reserved words, separated
by a plus. For example, "medium+short" means
{@link DateFormats#getDateTimeFormat} with the medium date styling and
the short time styling.
<p>In additions, the format could be a combination of the following pattern letters:
<table border=0 cellspacing=3 cellpadding=0>
<tr bgcolor="#ccccff">
<th align=left>Letter
<th align=left>Date or Time Component
<th align=left>Presentation
<th align=left>Examples
<tr>
<td><code>G</code>
<td>Era designator
<td><a href="#text">Text</a>
<td><code>AD</code>
<tr bgcolor="#eeeeff">
<td><code>y</code>
<td>Year
<td><a href="#year">Year</a>
<td><code>1996</code>; <code>96</code>
<tr>
<td><code>M</code>
<td>Month in year
<td><a href="#month">Month</a>
<td><code>July</code>; <code>Jul</code>; <code>07</code>
<tr bgcolor="#eeeeff">
<td><code>w</code>
<td>Week in year (starting at 1)
<td><a href="#number">Number</a>
<td><code>27</code>
<tr>
<td><code>W</code>
<td>Week in month (starting at 1)
<td><a href="#number">Number</a>
<td><code>2</code>
<tr bgcolor="#eeeeff">
<td><code>D</code>
<td>Day in year (starting at 1)
<td><a href="#number">Number</a>
<td><code>189</code>
<tr>
<td><code>d</code>
<td>Day in month (starting at 1)
<td><a href="#number">Number</a>
<td><code>10</code>
<tr bgcolor="#eeeeff">
<td><code>F</code>
<td>Day of week in month
<td><a href="#number">Number</a>
<td><code>2</code>
<tr>
<td><code>E</code>
<td>Day in week
<td><a href="#text">Text</a>
<td><code>Tuesday</code>; <code>Tue</code>
</table>
*/
public void setFormat(String format) throws WrongValueException {
if (format == null) {
format = "";
} else if (format.length() != 0) {
boolean bCustom;
int j = format.indexOf('+');
if (j > 0) {
bCustom = toStyle(format.substring(j + 1)) == -111 || toStyle(format.substring(0, j)) == -111;
} else {
bCustom = toStyle(format) == -111;
}
if (bCustom)
getDateFormat(format); // make sure the format is correct
}
super.setFormat(format);
smartUpdate("localizedFormat", getLocalizedFormat());
}
/** Returns the styling index, or -111 if not matched. */
public static int toStyle(String format) {
if ("short".equals(format = format.trim().toLowerCase(java.util.Locale.ENGLISH)))
return DateFormat.SHORT;
if ("medium".equals(format))
return DateFormat.MEDIUM;
if ("long".equals(format))
return DateFormat.LONG;
if ("full".equals(format))
return DateFormat.FULL;
return -111; //not found
}
/** Returns the real format, i.e., the combination of the format patterns,
* such as yyyy-MM-dd.
* <p>As described in {@link #setFormat}, a developer could specify
* an abstract name, such as short, or an empty string as the format,
* and this method will convert it to a real date/time format.
* @since 5.0.7
*/
public String getRealFormat() {
final String format = getFormat();
if (format == null || format.length() == 0)
return getDefaultFormat(); //backward compatible
int ds = format.indexOf('+');
if (ds > 0) {
int ts = toStyle(format.substring(ds + 1));
if (ts != -111) {
ds = toStyle(format.substring(0, ds));
if (ds != -111)
return DateFormats.getDateTimeFormat(ds, ts, _locale,
DEFAULT_FORMAT + " " + Timebox.DEFAULT_FORMAT);
}
} else {
ds = toStyle(format);
if (ds != -111)
return DateFormats.getDateFormat(ds, _locale, DEFAULT_FORMAT);
}
return format;
}
/** Sets the time zone that this component belongs to, or null if
* the default time zone is used.
* <p>The default time zone is determined by {@link TimeZones#getCurrent}.
*
* <p>Notice that if {@link #getDisplayedTimeZones} was called with
* a non-empty list, the time zone must be one of it.
* Otherwise (including <code>tzone</tt> is null),
* the first timezone is selected.
*/
public void setTimeZone(TimeZone tzone) {
if (_tzone != tzone) {
if (_dtzones != null) {
tzone = _dtzones.contains(tzone) ? tzone : _dtzones.get(0);
}
super.setTimeZone(tzone);
}
}
/**
* Returns a list of the time zones that will be displayed at the
* client and allow user to select.
* <p>Default: null
* @since 3.6.3
*/
public List<TimeZone> getDisplayedTimeZones() {
return _dtzones;
}
/**
* Sets a list of the time zones that will be displayed at the
* client and allow user to select.
* <p>If the {@link #getTimeZone()} is null,
* the first time zone in the list is assumed.
* @param dtzones a list of the time zones to display.
* If empty, it assumed to be null.
* @since 3.6.3
*/
public void setDisplayedTimeZones(List<TimeZone> dtzones) {
if (dtzones != null && dtzones.isEmpty())
dtzones = null;
if (_dtzones != dtzones) {
_dtzones = dtzones;
StringBuffer sb = new StringBuffer();
if (dtzones != null) {
int i = 0;
for (Iterator<TimeZone> it = dtzones.iterator(); it.hasNext(); i++) {
if (i != 0)
sb.append(',');
sb.append(it.next().getID());
}
}
smartUpdate("displayedTimeZones", sb.toString());
if (_tzone == null && _dtzones != null && _dtzones.get(0) != null)
_tzone = _dtzones.get(0);
}
}
/**
* Sets a concatenation of a list of the time zones' ID, separated by comma,
* that will be displayed at the client and allow user to select.
* <p>The time zone is retrieved by calling TimeZone.getTimeZone().
* @param dtzones a concatenation of a list of the timezones' ID, such as
* <code>"America/Los_Angeles,GMT+8"</code>
* @see #setDisplayedTimeZones(List)
* @since 3.6.3
*/
public void setDisplayedTimeZones(String dtzones) {
if (dtzones == null || dtzones.length() == 0) {
setDisplayedTimeZones((List<TimeZone>) null);
return;
}
LinkedList<TimeZone> list = new LinkedList<TimeZone>();
String[] ids = dtzones.split(",");
for (int i = 0; i < ids.length; i++) {
TimeZone tzone = TimeZone.getTimeZone(ids[i].trim());
if (tzone != null)
list.add(tzone);
}
setDisplayedTimeZones(list);
}
/**
* Returns whether the list of the time zones to display is readonly.
* If readonly, the user cannot change the time zone at the client.
* @since 3.6.3
*/
public boolean isTimeZonesReadonly() {
return _dtzonesReadonly;
}
/**
* Sets whether the list of the time zones to display is readonly.
* If readonly, the user cannot change the time zone at the client.
* @since 3.6.3
*/
public void setTimeZonesReadonly(boolean readonly) {
if (readonly != _dtzonesReadonly) {
_dtzonesReadonly = readonly;
smartUpdate("timeZonesReadonly", _dtzonesReadonly);
}
}
/**
* Internal use
* @hidden for Javadoc
*/
public static Map loadSymbols(Locale locale) {
WaitLock lock = null;
for (;;) {
final Object o;
synchronized (_symbols) {
o = _symbols.get(locale);
if (o == null)
_symbols.put(locale, lock = new WaitLock()); //lock it
}
if (o instanceof Map)
return (Map) o;
if (o == null)
break; //go to load the symbols
//wait because some one is creating the servlet
if (!((WaitLock) o).waitUntilUnlock(5 * 60 * 1000))
log.warn("Take too long to wait loading localized symbol: " + locale
+ "\nTry to load again automatically...");
} //for(;;)
try {
// the following implementation is referred to
// org.zkoss.zk.ui.http.Wpds#getDateJavaScript()
final Map<String, Object> map = new HashMap<String, Object>();
final Calendar cal = Calendar.getInstance(locale);
int firstDayOfWeek = Utils.getFirstDayOfWeek();
cal.clear();
if (firstDayOfWeek < 0)
firstDayOfWeek = cal.getFirstDayOfWeek();
map.put("DOW_1ST", Integer.valueOf(firstDayOfWeek - Calendar.SUNDAY));
map.put("MINDAYS", cal.getMinimalDaysInFirstWeek());
final boolean zhlang = locale.getLanguage().equals("zh");
SimpleDateFormat df = new SimpleDateFormat("E", locale);
final String[] sdow = new String[7], s2dow = new String[7];
for (int j = firstDayOfWeek, k = 0; k < 7; ++k) {
cal.set(Calendar.DAY_OF_WEEK, j);
sdow[k] = df.format(cal.getTime());
if (++j > Calendar.SATURDAY)
j = Calendar.SUNDAY;
if (zhlang) {
s2dow[k] = sdow[k].length() >= 3 ? sdow[k].substring(2) : sdow[k];
} else {
final int len = sdow[k].length();
final char cc = sdow[k].charAt(len - 1);
s2dow[k] = cc == '.' || cc == ',' ? sdow[k].substring(0, len - 1) : sdow[k];
}
}
map.put("LAN_TAG", locale.toLanguageTag());
Map<String, Map<String, Integer>> eras = new HashMap<String, Map<String, Integer>>();
Chronology chronology = Chronology.ofLocale(locale);
String format = "G-yyyy/MM/dd";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format, locale);
String ce = IsoEra.CE.getDisplayName(TextStyle.SHORT, locale);
String bce = IsoEra.BCE.getDisplayName(TextStyle.SHORT, locale);
List<Era> eraList = chronology.eras();
for (Era era : eraList) {
try {
String eraName = era.getDisplayName(TextStyle.SHORT, locale);
// MinguoEra displayName might be wrong with old version JDK (e.g. openJDK8)
if (era instanceof MinguoEra) {
if (eraName.equalsIgnoreCase(ce))
eraName = dtf.format(MinguoDate.of(1, 1, 1)).split("-")[0];
else if (eraName.equalsIgnoreCase(bce))
eraName = dtf.format(MinguoDate.of(-1, 1, 1)).split("-")[0];
}
Map<String, Integer> eraData = new HashMap<String, Integer>(2);
int firstYear;
if (era instanceof JapaneseEra) {
// In JapaneseDate, Only Meiji and later eras are supported; dates before Meiji 6, January 1 are not supported.
firstYear = LocalDate.parse(eraName + "-0006/01/01", dtf.withChronology(chronology)).minusYears(5).getYear();
eraData.put("firstYear", firstYear);
eraData.put("direction", 1); // JP era year counting direction never be negative
} else {
firstYear = LocalDate.parse(eraName + "-0001/01/01", dtf.withChronology(chronology)).getYear();
eraData.put("firstYear", firstYear);
eraData.put("direction", era.getValue() <= 0 ? -1 : 1); // era year counting direction
}
eras.put(eraName, eraData);
} catch (DateTimeParseException e) {
log.warn("LocalizedSymbols ERAS parsing failed with Locale: " + locale + "and Era: " + era);
}
}
map.put("ERAS", eras);
if (locale.getCountry().equals("TH")) { // keep ThaiBuddhist works in ZK CE
df = new SimpleDateFormat("G", locale);
map.put("ERA", df.format(new java.util.Date()));
Calendar ec = Calendar.getInstance(Locale.ENGLISH);
Calendar lc = Calendar.getInstance(locale);
map.put("YDELTA", Integer.valueOf(lc.get(Calendar.YEAR) - ec.get(Calendar.YEAR)));
} else {
map.put("ERA", "");
map.put("YDELTA", 0);
}
df = new SimpleDateFormat("EEEE", locale);
final String[] fdow = new String[7];
for (int j = firstDayOfWeek, k = 0; k < 7; ++k) {
cal.set(Calendar.DAY_OF_WEEK, j);
fdow[k] = df.format(cal.getTime());
if (++j > Calendar.SATURDAY)
j = Calendar.SUNDAY;
}
df = new SimpleDateFormat("MMM", locale);
final String[] smon = new String[12], s2mon = new String[12];
for (int j = 0; j < 12; ++j) {
cal.set(Calendar.MONTH, j);
smon[j] = df.format(cal.getTime());
if (zhlang) {
s2mon[j] = smon[0].length() >= 2 // remove the last char
? smon[j].substring(0, smon[j].length() - 1) : smon[j];
} else {
final int len = smon[j].length();
final char cc = smon[j].charAt(len - 1);
s2mon[j] = cc == '.' || cc == ',' ? smon[j].substring(0, len - 1) : smon[j];
}
}
df = new SimpleDateFormat("MMMM", locale);
final String[] fmon = new String[12];
for (int j = 0; j < 12; ++j) {
cal.set(Calendar.MONTH, j);
fmon[j] = df.format(cal.getTime());
}
map.put("SDOW", sdow);
if (Objects.equals(s2dow, sdow))
map.put("S2DOW", sdow);
else
map.put("S2DOW", s2dow);
if (Objects.equals(fdow, sdow))
map.put("FDOW", sdow);
else
map.put("FDOW", fdow);
map.put("SMON", smon);
if (Objects.equals(s2mon, smon))
map.put("S2MON", smon);
else
map.put("S2MON", s2mon);
if (Objects.equals(fmon, smon))
map.put("FMON", smon);
else
map.put("FMON", fmon);
// AM/PM available since ZK 3.0
df = new SimpleDateFormat("a", locale);
cal.set(Calendar.HOUR_OF_DAY, 3);
final String[] ampm = new String[2];
ampm[0] = df.format(cal.getTime());
cal.set(Calendar.HOUR_OF_DAY, 15);
ampm[1] = df.format(cal.getTime());
map.put("APM", ampm);
synchronized (_symbols) {
_symbols.put(locale, map);
cloneSymbols();
}
return map;
} finally {
lock.unlock();
}
}
private static void cloneSymbols() {
final Map<Locale, Object> symbols = new HashMap<Locale, Object>();
for (Map.Entry<Locale, Object> me : _symbols.entrySet()) {
final Object value = me.getValue();
if (value instanceof Map)
symbols.put(me.getKey(), value);
}
_symbols = symbols;
}
private static Object[] getRealSymbols(Locale locale, Datebox box) {
if (locale != null) {
final String localeName = locale.toString();
if (org.zkoss.zk.ui.impl.Utils.markClientInfoPerDesktop(box.getDesktop(),
box.getClass().getName() + localeName)) {
Map symbols = (Map) _symbols.get(locale);
if (symbols == null)
symbols = loadSymbols(locale);
return new Object[] { localeName, symbols };
}
return new Object[] { localeName, null };
}
return null;
}
/**
* Drops down or closes the calendar to select a date.
* only works while visible
* @since 3.0.1
* @see #open
* @see #close
*/
public void setOpen(boolean open) {
if (isVisible()) {
if (open)
open();
else
close();
}
}
/**
* Drops down the calendar to select a date. The same as setOpen(true).
*
* @since 3.0.1
*/
public void open() {
response("open", new AuInvoke(this, "setOpen", true)); //don't use smartUpdate
}
/**
* Closes the calendar if it was dropped down. The same as setOpen(false).
*
* @since 3.0.1
*/
public void close() {
response("open", new AuInvoke(this, "setOpen", false)); //don't use smartUpdate
}
/** Processes an AU request.
*
* <p>Default: in addition to what are handled by {@link XulElement#service},
* it also handles onTimeZoneChange, onChange, onChanging and onError.
* @since 5.0.0
*/
public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
final String cmd = request.getCommand();
if (cmd.equals(Events.ON_TIME_ZONE_CHANGE)) {
final Map<String, Object> data = request.getData();
String timezone = (String) data.get("timezone");
setTimeZone(timezone);
} else
super.service(request, everError);
}
public Object getExtraCtrl() {
return new ExtraCtrl();
}
/** A utility class to implement {@link #getExtraCtrl}.
* It is used only by component developers.
*
* <p>If a component requires more client controls, it is suggested to
* override {@link #getExtraCtrl} to return an instance that extends from
* this class.
*/
protected class ExtraCtrl extends FormatInputElement.ExtraCtrl implements Blockable {
public boolean shallBlock(AuRequest request) {
// B50-3316103: special case of readonly component: do not block onChange and onSelect
final String cmd = request.getCommand();
if (Events.ON_OPEN.equals(cmd))
return false;
return isDisabled() || (isReadonly() && Events.ON_CHANGING.equals(cmd))
|| !Components.isRealVisible(Datebox.this);
}
}
/**
* @param constr a list of constraints separated by comma.
* Example: "between 20071012 and 20071223", "before 20080103"
*/
// -- super --//
public void setConstraint(String constr) {
setConstraint(constr != null ? new SimpleDateConstraint(constr) : null); // Bug 2564298
}
protected Object coerceFromString(String value) throws WrongValueException {
if (value == null || value.length() == 0)
return null;
final String fmt = getRealFormat();
final DateFormat df = getDateFormat(fmt);
df.setLenient(_lenient);
final Date date;
try {
date = df.parse(value);
} catch (ParseException ex) {
throw showCustomError(new WrongValueException(this, MZul.DATE_REQUIRED, new Object[] { value, fmt }));
}
return date;
}
protected String coerceToString(Object value) {
if (value == null)
return "";
if (value instanceof Date) {
final DateFormat df = getDateFormat(getRealFormat());
return df.format((Date) value);
}
// ZK-631, will receive the "wrong" string value
// if set both custom constraint and format
// for showCustomError
throw showCustomError(
new WrongValueException(this, MZul.DATE_REQUIRED, new Object[] { value, getRealFormat() }));
}
/**
* Returns the date format of the specified format
*
* <p>
* Default: it uses SimpleDateFormat to format the date.
*
* @param fmt
* the pattern.
*/
protected DateFormat getDateFormat(String fmt) {
final DateFormat df = new SimpleDateFormat(fmt, _locale != null ? _locale : Locales.getCurrent());
final TimeZone tz = _tzone != null ? _tzone : TimeZones.getCurrent();
df.setTimeZone(tz);
return df;
}
private String getUnformater() {
if (org.zkoss.zk.ui.impl.Utils.markClientInfoPerDesktop(getDesktop(),
"org.zkoss.zul.Datebox.unformater.isSent")) {
return Library.getProperty("org.zkoss.zul.Datebox.unformater");
}
return null;
}
public String getZclass() {
return _zclass == null ? "z-datebox" : _zclass;
}
/**
* Returns whether enable to show the link that jump to today in day view
* <p>Default: false
* @since 8.0.0
* @return boolean
*/
public boolean getShowTodayLink() {
return _showTodayLink;
}
/**
* Sets whether enable to show the link that jump to today in day view
* @param showTodayLink show or hidden
* @since 8.0.0
*/
public void setShowTodayLink(boolean showTodayLink) {
if (_showTodayLink != showTodayLink) {
_showTodayLink = showTodayLink;
smartUpdate("showTodayLink", _showTodayLink);
}
}
/**
* Returns the label of the link that jump to today in day view
* <p>Default: Today
* @since 8.0.4
* @return String
*/
public String getTodayLinkLabel() {
return _todayLinkLabel;
}
/**
* Sets the label of the link that jump to today in day view
* @param todayLinkLabel today link label
* @since 8.0.4
*/
public void setTodayLinkLabel(String todayLinkLabel) {
if (!Objects.equals(_todayLinkLabel, todayLinkLabel)) {
_todayLinkLabel = todayLinkLabel;
smartUpdate("todayLinkLabel", todayLinkLabel);
}
}
/**
* Returns the default datetime if the value is empty.
* <p>Default: null (means current datetime)
* @since 9.0.0
*/
public LocalDateTime getDefaultDateTime() {
return _defaultDateTime;
}
/**
* Sets the default datetime if the value is empty.
* @param defaultDateTime Default datetime. null means current datetime.
* @since 9.0.0
*/
public void setDefaultDateTime(LocalDateTime defaultDateTime) {
if (_defaultDateTime != defaultDateTime) {
_defaultDateTime = defaultDateTime;
smartUpdate("defaultDateTime", toDate(_defaultDateTime));
}
}
/**
* Returns the level that a user can select.
* <p>
* Default: "day"
* @return the level that a user can select
* @since 9.5.1
*/
public String getSelectLevel() {
return _selectLevel;
}
/**
* Sets the level that a user can select.
* The valid options are "day", "month", and "year".
*
* @param selectLevel the level that a user can select
* @since 9.5.1
*/
public void setSelectLevel(String selectLevel) {
if (!"day".equals(selectLevel) && !"month".equals(selectLevel) && !"year".equals(selectLevel))
throw new WrongValueException("Only allowed day, month, and year, not " + selectLevel);
if (!Objects.equals(_selectLevel, selectLevel)) {
_selectLevel = selectLevel;
smartUpdate("selectLevel", selectLevel);
}
}
/**
* Returns whether to auto close the datebox popup after changing the timezone.
* <p>
* Default: true
* @since 9.6.0
* @return boolean
*/
public boolean getClosePopupOnTimezoneChange() {
return _closePopupOnTimezoneChange;
}
/**
* Sets whether to auto close the datebox popup after changing the timezone.
* @param closePopupOnTimezoneChange shall close the datebox popup or not
* @since 9.6.0
*/
public void setClosePopupOnTimezoneChange(boolean closePopupOnTimezoneChange) {
if (_closePopupOnTimezoneChange != closePopupOnTimezoneChange) {
_closePopupOnTimezoneChange = closePopupOnTimezoneChange;
smartUpdate("closePopupOnTimezoneChange", _closePopupOnTimezoneChange);
}
}
//--ComponentCtrl--//
private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(2);
static {
_properties.put("buttonVisible", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Datebox) cmp).setButtonVisible(value);
}
public Boolean getValue(Component cmp) {
return ((Datebox) cmp).isButtonVisible();
}
});
_properties.put("lenient", new BooleanPropertyAccess() {
public void setValue(Component cmp, Boolean value) {
((Datebox) cmp).setLenient(value);
}
public Boolean getValue(Component cmp) {
return ((Datebox) cmp).isLenient();
}
});
}
public PropertyAccess getPropertyAccess(String prop) {
PropertyAccess pa = _properties.get(prop);
if (pa != null)
return pa;
return super.getPropertyAccess(prop);
}
protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
super.renderProperties(renderer);
if (!_btnVisible)
renderer.render("buttonVisible", false);
if (!_lenient)
renderer.render("lenient", false);
if (_dtzonesReadonly)
renderer.render("timeZonesReadonly", true);
if (_dtzones != null) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < _dtzones.size(); i++) {
if (i != 0)
sb.append(',');
TimeZone tz = _dtzones.get(i);
sb.append(tz.getID());
}
renderer.render("displayedTimeZones", sb.toString());
}
render(renderer, "weekOfYear", _weekOfYear);
render(renderer, "position", _position);
renderer.render("localizedFormat", getLocalizedFormat());
String unformater = getUnformater();
if (!Strings.isBlank(unformater))
renderer.render("unformater", unformater);
if (_locale != null)
renderer.render("localizedSymbols", getRealSymbols(_locale, this));
if (_strictDate)
render(renderer, "strictDate", _strictDate);
render(renderer, "showTodayLink", _showTodayLink);
render(renderer, "todayLinkLabel", _todayLinkLabel);
if (_defaultDateTime != null)
render(renderer, "defaultDateTime", toDate(_defaultDateTime));
if (!"day".equals(_selectLevel))
render(renderer, "selectLevel", _selectLevel);
if (!_closePopupOnTimezoneChange)
renderer.render("closePopupOnTimezoneChange", false);
}
}