re PR libgcj/17002 (java.util.TimeZone.getDefault() is broken)

libjava/
	PR libgcj/17002
	PR classpath/28550
	* java/util/VMTimeZone.java (getDefaultTimeZoneId): To read
	/etc/localtime, use ZoneInfo.readTZFile instead of
	VMTimeZone.readtzFile.  Get better timezone name for /etc/localtime,
	either if it is a symlink or through /etc/sysconfig/clock.
	(readSysconfigClockFile): New static method.
	(readtzFile): Removed.
	* java/lang/System.java: Add gnu.java.util.zoneinfo.dir to comments.
	* posix.cc (_Jv_platform_initProperties): Set
	gnu.java.util.zoneinfo.dir.
	* sources.am (gnu_java_util_source_files): Add
	classpath/gnu/java/util/ZoneInfo.java.
	* Makefile.in: Regenerated.
	* java/util/VMTimeZone.h: Regenerated.
	* java/util/TimeZone.h: Regenerated.
	* gnu/java/util/ZoneInfo.h: Generated.
libjava/classpath/
	* java/util/Date.java (parse): Properly parse 09:01:02 as
	hours/minutes/seconds, not as hours/minutes/year.
	* java/util/SimpleTimeZone.java (SimpleTimeZone): Simplify
	{start,end}TimeMode constructor by calling shorter constructor,
	set {start,end}TimeMode fields after it returns.
	(setStartRule): Don't adjust startTime into WALL_TIME.  Set
	startTimeMode to WALL_TIME.
	(endStartRule): Similarly.
	(getOffset): Handle properly millis + dstOffset overflowing into the
	next day.  Adjust startTime resp. endTime based on startTimeMode
	resp. endTimeMode.
	* java/util/TimeZone.java (zoneinfo_dir, availableIDs, aliases0): New
	static fields.
	(timezones): Remove synchronized keyword.  Set zoneinfo_dir.
	If non-null, set up aliases0 and don't put anything into
	timezones0.
	(defaultZone): Call getTimeZone instead of timezones().get.
	(getDefaultTimeZone): Fix parsing of EST5 or EST5EDT6.  Use
	getTimeZoneInternal instead of timezones().get.
	(parseTime): Parse correctly hour:minute.
	(getTimeZoneInternal): New private method.
	(getTimeZone): Do the custom ID checking first, canonicalize
	ID for custom IDs as required by documentation.  Call
	getTimeZoneInternal to handle the rest.
	(getAvailableIDs(int)): Add locking.  Handle zoneinfo_dir != null.
	(getAvailableIDs(File,String,ArrayList)): New private method.
	(getAvailableIDs()): Add locking.  Handle zoneinfo_dir != null.
	* gnu/java/util/ZoneInfo.java: New file.

From-SVN: r122229
This commit is contained in:
Jakub Jelinek
2007-02-22 17:04:55 +01:00
committed by Jakub Jelinek
parent 0c5c188f07
commit b3502aa8d4
20 changed files with 1702 additions and 607 deletions
+9
View File
@@ -754,6 +754,7 @@ public class Date
}
else if (firstch >= '0' && firstch <= '9')
{
int lastPunct = -1;
while (tok != null && tok.length() > 0)
{
int punctOffset = tok.length();
@@ -791,6 +792,13 @@ public class Date
else
minute = num;
}
else if (lastPunct == ':' && hour >= 0 && (minute < 0 || second < 0))
{
if (minute < 0)
minute = num;
else
second = num;
}
else if ((num >= 70
&& (punct == ' ' || punct == ','
|| punct == '/' || punct < 0))
@@ -828,6 +836,7 @@ public class Date
tok = null;
else
tok = tok.substring(punctOffset + 1);
lastPunct = punct;
}
}
else if (firstch >= 'A' && firstch <= 'Z')
+46 -72
View File
@@ -1,5 +1,6 @@
/* java.util.SimpleTimeZone
Copyright (C) 1998, 1999, 2000, 2003, 2004, 2005 Free Software Foundation, Inc.
Copyright (C) 1998, 1999, 2000, 2003, 2004, 2005, 2007
Free Software Foundation, Inc.
This file is part of GNU Classpath.
@@ -141,8 +142,8 @@ public class SimpleTimeZone extends TimeZone
/**
* This variable specifies the time of change to daylight savings.
* This time is given in milliseconds after midnight local
* standard time.
* This time is given in milliseconds after midnight in startTimeMode
* chosen time mode.
* @serial
*/
private int startTime;
@@ -187,8 +188,8 @@ public class SimpleTimeZone extends TimeZone
/**
* This variable specifies the time of change back to standard time.
* This time is given in milliseconds after midnight local
* standard time.
* This time is given in milliseconds after midnight in endTimeMode
* chosen time mode.
* @serial
*/
private int endTime;
@@ -380,24 +381,17 @@ public class SimpleTimeZone extends TimeZone
int endDayOfWeekInMonth, int endDayOfWeek,
int endTime, int endTimeMode, int dstSavings)
{
this.rawOffset = rawOffset;
setID(id);
useDaylight = true;
this(rawOffset, id, startMonth, startDayOfWeekInMonth, startDayOfWeek,
startTime, endMonth, endDayOfWeekInMonth, endDayOfWeek, endTime);
if (startTimeMode < WALL_TIME || startTimeMode > UTC_TIME)
throw new IllegalArgumentException("startTimeMode must be one of WALL_TIME, STANDARD_TIME, or UTC_TIME");
if (endTimeMode < WALL_TIME || endTimeMode > UTC_TIME)
throw new IllegalArgumentException("endTimeMode must be one of WALL_TIME, STANDARD_TIME, or UTC_TIME");
this.startTimeMode = startTimeMode;
this.endTimeMode = endTimeMode;
setStartRule(startMonth, startDayOfWeekInMonth, startDayOfWeek, startTime);
setEndRule(endMonth, endDayOfWeekInMonth, endDayOfWeek, endTime);
if (startMonth == endMonth)
throw new IllegalArgumentException("startMonth and endMonth must be different");
this.startYear = 0;
this.dstSavings = dstSavings;
this.startTimeMode = startTimeMode;
this.endTimeMode = endTimeMode;
}
/**
@@ -477,12 +471,8 @@ public class SimpleTimeZone extends TimeZone
this.startMonth = month;
this.startDay = day;
this.startDayOfWeek = Math.abs(dayOfWeek);
if (this.startTimeMode == WALL_TIME || this.startTimeMode == STANDARD_TIME)
this.startTime = time;
else
// Convert from UTC to STANDARD
this.startTime = time + this.rawOffset;
useDaylight = true;
this.startTime = time;
this.startTimeMode = WALL_TIME;
}
/**
@@ -513,24 +503,10 @@ public class SimpleTimeZone extends TimeZone
public void setStartRule(int month, int day, int dayOfWeek, int time,
boolean after)
{
// FIXME: XXX: Validate that checkRule and offset processing work with on
// or before mode.
this.startDay = after ? Math.abs(day) : -Math.abs(day);
this.startDayOfWeek = after ? Math.abs(dayOfWeek) : -Math.abs(dayOfWeek);
this.startMode = (dayOfWeek != 0)
? (after ? DOW_GE_DOM_MODE : DOW_LE_DOM_MODE)
: checkRule(month, day, dayOfWeek);
this.startDay = Math.abs(this.startDay);
this.startDayOfWeek = Math.abs(this.startDayOfWeek);
this.startMonth = month;
if (this.startTimeMode == WALL_TIME || this.startTimeMode == STANDARD_TIME)
this.startTime = time;
if (after)
setStartRule(month, day, -dayOfWeek, time);
else
// Convert from UTC to STANDARD
this.startTime = time + this.rawOffset;
useDaylight = true;
setStartRule(month, -day, -dayOfWeek, time);
}
/**
@@ -570,14 +546,8 @@ public class SimpleTimeZone extends TimeZone
this.endMonth = month;
this.endDay = day;
this.endDayOfWeek = Math.abs(dayOfWeek);
if (this.endTimeMode == WALL_TIME)
this.endTime = time;
else if (this.endTimeMode == STANDARD_TIME)
// Convert from STANDARD to DST
this.endTime = time + this.dstSavings;
else
// Convert from UTC to DST
this.endTime = time + this.rawOffset + this.dstSavings;
this.endTime = time;
this.endTimeMode = WALL_TIME;
useDaylight = true;
}
@@ -607,27 +577,10 @@ public class SimpleTimeZone extends TimeZone
public void setEndRule(int month, int day, int dayOfWeek, int time,
boolean after)
{
// FIXME: XXX: Validate that checkRule and offset processing work with on
// or before mode.
this.endDay = after ? Math.abs(day) : -Math.abs(day);
this.endDayOfWeek = after ? Math.abs(dayOfWeek) : -Math.abs(dayOfWeek);
this.endMode = (dayOfWeek != 0)
? (after ? DOW_GE_DOM_MODE : DOW_LE_DOM_MODE)
: checkRule(month, day, dayOfWeek);
this.endDay = Math.abs(this.endDay);
this.endDayOfWeek = Math.abs(endDayOfWeek);
this.endMonth = month;
if (this.endTimeMode == WALL_TIME)
this.endTime = time;
else if (this.endTimeMode == STANDARD_TIME)
// Convert from STANDARD to DST
this.endTime = time + this.dstSavings;
if (after)
setEndRule(month, day, -dayOfWeek, time);
else
// Convert from UTC to DST
this.endTime = time + this.rawOffset + this.dstSavings;
useDaylight = true;
setEndRule(month, -day, -dayOfWeek, time);
}
/**
@@ -688,16 +641,37 @@ public class SimpleTimeZone extends TimeZone
int daylightSavings = 0;
if (useDaylight && era == GregorianCalendar.AD && year >= startYear)
{
int orig_year = year;
int time = startTime + (startTimeMode == UTC_TIME ? rawOffset : 0);
// This does only work for Gregorian calendars :-(
// This is mainly because setStartYear doesn't take an era.
boolean afterStart = ! isBefore(year, month, day, dayOfWeek, millis,
startMode, startMonth, startDay,
startDayOfWeek, startTime);
boolean beforeEnd = isBefore(year, month, day, dayOfWeek,
millis + dstSavings,
endMode, endMonth, endDay, endDayOfWeek,
endTime);
startDayOfWeek, time);
millis += dstSavings;
if (millis >= 24 * 60 * 60 * 1000)
{
millis -= 24 * 60 * 60 * 1000;
dayOfWeek = (dayOfWeek % 7) + 1;
if (++day > daysInMonth)
{
day = 1;
if (month++ == Calendar.DECEMBER)
{
month = Calendar.JANUARY;
year++;
}
}
}
time = endTime + (endTimeMode == UTC_TIME ? rawOffset : 0);
if (endTimeMode != WALL_TIME)
time += dstSavings;
boolean beforeEnd = isBefore(year, month, day, dayOfWeek, millis,
endMode, endMonth, endDay, endDayOfWeek,
time);
if (year != orig_year)
afterStart = false;
if (startMonth < endMonth)
// use daylight savings, if the date is after the start of
// savings, and before the end of savings.
+261 -53
View File
@@ -39,6 +39,9 @@ exception statement from your version. */
package java.util;
import gnu.classpath.SystemProperties;
import gnu.java.util.ZoneInfo;
import java.io.File;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.DateFormatSymbols;
@@ -115,7 +118,7 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
// Fall back on GMT.
if (zone == null)
zone = (TimeZone) timezones().get("GMT");
zone = getTimeZone ("GMT");
return zone;
}
@@ -127,6 +130,22 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
private static final long serialVersionUID = 3581463369166924961L;
/**
* Flag whether zoneinfo data should be used,
* otherwise builtin timezone data will be provided.
*/
private static String zoneinfo_dir;
/**
* Cached copy of getAvailableIDs().
*/
private static String[] availableIDs = null;
/**
* JDK 1.1.x compatibility aliases.
*/
private static HashMap aliases0;
/**
* HashMap for timezones by ID.
*/
@@ -135,13 +154,55 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
* it is not needed:
*/
// Package-private to avoid a trampoline.
static synchronized HashMap timezones()
static HashMap timezones()
{
if (timezones0 == null)
{
HashMap timezones = new HashMap();
timezones0 = timezones;
zoneinfo_dir = SystemProperties.getProperty("gnu.java.util.zoneinfo.dir");
if (zoneinfo_dir != null && !new File(zoneinfo_dir).isDirectory())
zoneinfo_dir = null;
if (zoneinfo_dir != null)
{
aliases0 = new HashMap();
// These deprecated aliases for JDK 1.1.x compatibility
// should take precedence over data files read from
// /usr/share/zoneinfo.
aliases0.put("ACT", "Australia/Darwin");
aliases0.put("AET", "Australia/Sydney");
aliases0.put("AGT", "America/Argentina/Buenos_Aires");
aliases0.put("ART", "Africa/Cairo");
aliases0.put("AST", "America/Juneau");
aliases0.put("BST", "Asia/Colombo");
aliases0.put("CAT", "Africa/Gaborone");
aliases0.put("CNT", "America/St_Johns");
aliases0.put("CST", "CST6CDT");
aliases0.put("CTT", "Asia/Brunei");
aliases0.put("EAT", "Indian/Comoro");
aliases0.put("ECT", "CET");
aliases0.put("EST", "EST5EDT");
aliases0.put("EST5", "EST5EDT");
aliases0.put("IET", "EST5EDT");
aliases0.put("IST", "Asia/Calcutta");
aliases0.put("JST", "Asia/Seoul");
aliases0.put("MIT", "Pacific/Niue");
aliases0.put("MST", "MST7MDT");
aliases0.put("MST7", "MST7MDT");
aliases0.put("NET", "Indian/Mauritius");
aliases0.put("NST", "Pacific/Auckland");
aliases0.put("PLT", "Indian/Kerguelen");
aliases0.put("PNT", "MST7MDT");
aliases0.put("PRT", "America/Anguilla");
aliases0.put("PST", "PST8PDT");
aliases0.put("SST", "Pacific/Ponape");
aliases0.put("VST", "Asia/Bangkok");
return timezones;
}
TimeZone tz;
// Automatically generated by scripts/timezones.pl
// XXX - Should we read this data from a file?
@@ -887,7 +948,6 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
static TimeZone getDefaultTimeZone(String sysTimeZoneId)
{
String stdName = null;
String dstName;
int stdOffs;
int dstOffs;
try
@@ -900,14 +960,14 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
// get std
do
c = sysTimeZoneId.charAt(index++);
c = sysTimeZoneId.charAt(index);
while (c != '+' && c != '-' && c != ',' && c != ':'
&& ! Character.isDigit(c) && c != '\0' && index < idLength);
&& ! Character.isDigit(c) && c != '\0' && ++index < idLength);
if (index >= idLength)
return (TimeZone)timezones().get(sysTimeZoneId);
return getTimeZoneInternal(sysTimeZoneId);
stdName = sysTimeZoneId.substring(0, --index);
stdName = sysTimeZoneId.substring(0, index);
prevIndex = index;
// get the std offset
@@ -938,7 +998,7 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
if (index >= idLength)
{
// Do we have an existing timezone with that name and offset?
TimeZone tz = (TimeZone) timezones().get(stdName);
TimeZone tz = getTimeZoneInternal(stdName);
if (tz != null)
if (tz.getRawOffset() == stdOffs)
return tz;
@@ -949,16 +1009,16 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
// get dst
do
c = sysTimeZoneId.charAt(index++);
c = sysTimeZoneId.charAt(index);
while (c != '+' && c != '-' && c != ',' && c != ':'
&& ! Character.isDigit(c) && c != '\0' && index < idLength);
&& ! Character.isDigit(c) && c != '\0' && ++index < idLength);
// Done yet? (Format: std offset dst)
if (index >= idLength)
{
// Do we have an existing timezone with that name and offset
// which has DST?
TimeZone tz = (TimeZone) timezones().get(stdName);
TimeZone tz = getTimeZoneInternal(stdName);
if (tz != null)
if (tz.getRawOffset() == stdOffs && tz.useDaylightTime())
return tz;
@@ -968,7 +1028,6 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
}
// get the dst offset
dstName = sysTimeZoneId.substring(prevIndex, --index);
prevIndex = index;
do
c = sysTimeZoneId.charAt(index++);
@@ -1005,7 +1064,7 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
if (index >= idLength)
{
// Time Zone existing with same name, dst and offsets?
TimeZone tz = (TimeZone) timezones().get(stdName);
TimeZone tz = getTimeZoneInternal(stdName);
if (tz != null)
if (tz.getRawOffset() == stdOffs && tz.useDaylightTime()
&& tz.getDSTSavings() == (dstOffs - stdOffs))
@@ -1171,10 +1230,10 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
break;
else
i++;
millis += 60 * 1000 * Integer.parseInt(time.substring(iprev, i));
if (i >= time.length())
return millis;
millis += 60 * 1000 * Integer.parseInt(time.substring(iprev, i));
millis += 1000 * Integer.parseInt(time.substring(++i));
return millis;
}
@@ -1406,30 +1465,67 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
* @return The time zone for the identifier or GMT, if no such time
* zone exists.
*/
// FIXME: XXX: JCL indicates this and other methods are synchronized.
public static TimeZone getTimeZone(String ID)
private static TimeZone getTimeZoneInternal(String ID)
{
// First check timezones hash
TimeZone tz = (TimeZone) timezones().get(ID);
if (tz != null)
TimeZone tz = null;
TimeZone tznew = null;
for (int pass = 0; pass < 2; pass++)
{
if (tz.getID().equals(ID))
return tz;
synchronized (TimeZone.class)
{
tz = (TimeZone) timezones().get(ID);
if (tz != null)
{
if (!tz.getID().equals(ID))
{
// We always return a timezone with the requested ID.
// This is the same behaviour as with JDK1.2.
tz = (TimeZone) tz.clone();
tz.setID(ID);
// We also save the alias, so that we return the same
// object again if getTimeZone is called with the same
// alias.
timezones().put(ID, tz);
}
return tz;
}
else if (tznew != null)
{
timezones().put(ID, tznew);
return tznew;
}
}
// We always return a timezone with the requested ID.
// This is the same behaviour as with JDK1.2.
tz = (TimeZone) tz.clone();
tz.setID(ID);
// We also save the alias, so that we return the same
// object again if getTimeZone is called with the same
// alias.
timezones().put(ID, tz);
return tz;
if (pass == 1 || zoneinfo_dir == null)
return null;
// aliases0 is never changing after first timezones(), so should
// be safe without synchronization.
String zonename = (String) aliases0.get(ID);
if (zonename == null)
zonename = ID;
// Read the file outside of the critical section, it is expensive.
tznew = ZoneInfo.readTZFile (ID, zoneinfo_dir
+ File.separatorChar + zonename);
if (tznew == null)
return null;
}
// See if the ID is really a GMT offset form.
// Note that GMT is in the table so we know it is different.
if (ID.startsWith("GMT"))
return null;
}
/**
* Gets the TimeZone for the given ID.
* @param ID the time zone identifier.
* @return The time zone for the identifier or GMT, if no such time
* zone exists.
*/
public static TimeZone getTimeZone(String ID)
{
// Check for custom IDs first
if (ID.startsWith("GMT") && ID.length() > 3)
{
int pos = 3;
int offset_direction = 1;
@@ -1474,8 +1570,20 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
}
}
return new SimpleTimeZone((hour * (60 * 60 * 1000) +
minute * (60 * 1000))
// Custom IDs have to be normalized
StringBuffer sb = new StringBuffer(9);
sb.append("GMT");
sb.append(offset_direction >= 0 ? '+' : '-');
sb.append((char) ('0' + hour / 10));
sb.append((char) ('0' + hour % 10));
sb.append(':');
sb.append((char) ('0' + minute / 10));
sb.append((char) ('0' + minute % 10));
ID = sb.toString();
return new SimpleTimeZone((hour * (60 * 60 * 1000)
+ minute * (60 * 1000))
* offset_direction, ID);
}
catch (NumberFormatException e)
@@ -1483,8 +1591,11 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
}
}
// Finally, return GMT per spec
return getTimeZone("GMT");
TimeZone tz = getTimeZoneInternal(ID);
if (tz != null)
return tz;
return new SimpleTimeZone(0, "GMT");
}
/**
@@ -1497,37 +1608,134 @@ public abstract class TimeZone implements java.io.Serializable, Cloneable
*/
public static String[] getAvailableIDs(int rawOffset)
{
int count = 0;
Iterator iter = timezones().entrySet().iterator();
while (iter.hasNext())
synchronized (TimeZone.class)
{
// Don't iterate the values, since we want to count
// doubled values (aliases)
Map.Entry entry = (Map.Entry) iter.next();
if (((TimeZone) entry.getValue()).getRawOffset() == rawOffset)
count++;
HashMap h = timezones();
int count = 0;
if (zoneinfo_dir == null)
{
Iterator iter = h.entrySet().iterator();
while (iter.hasNext())
{
// Don't iterate the values, since we want to count
// doubled values (aliases)
Map.Entry entry = (Map.Entry) iter.next();
if (((TimeZone) entry.getValue()).getRawOffset() == rawOffset)
count++;
}
String[] ids = new String[count];
count = 0;
iter = h.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry = (Map.Entry) iter.next();
if (((TimeZone) entry.getValue()).getRawOffset() == rawOffset)
ids[count++] = (String) entry.getKey();
}
return ids;
}
}
String[] s = getAvailableIDs();
int count = 0;
for (int i = 0; i < s.length; i++)
{
TimeZone t = getTimeZoneInternal(s[i]);
if (t == null || t.getRawOffset() != rawOffset)
s[i] = null;
else
count++;
}
String[] ids = new String[count];
count = 0;
iter = timezones().entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry = (Map.Entry) iter.next();
if (((TimeZone) entry.getValue()).getRawOffset() == rawOffset)
ids[count++] = (String) entry.getKey();
}
for (int i = 0; i < s.length; i++)
if (s[i] != null)
ids[count++] = s[i];
return ids;
}
private static int getAvailableIDs(File d, String prefix, ArrayList list)
{
String[] files = d.list();
int count = files.length;
boolean top = prefix.length() == 0;
list.add (files);
for (int i = 0; i < files.length; i++)
{
if (top
&& (files[i].equals("posix")
|| files[i].equals("right")
|| files[i].endsWith(".tab")
|| aliases0.get(files[i]) != null))
{
files[i] = null;
count--;
continue;
}
File f = new File(d, files[i]);
if (f.isDirectory())
{
count += getAvailableIDs(f, prefix + files[i]
+ File.separatorChar, list) - 1;
files[i] = null;
}
else
files[i] = prefix + files[i];
}
return count;
}
/**
* Gets all available IDs.
* @return An array of all supported IDs.
*/
public static String[] getAvailableIDs()
{
return (String[])
timezones().keySet().toArray(new String[timezones().size()]);
synchronized (TimeZone.class)
{
HashMap h = timezones();
if (zoneinfo_dir == null)
return (String[]) h.keySet().toArray(new String[h.size()]);
if (availableIDs != null)
{
String[] ids = new String[availableIDs.length];
for (int i = 0; i < availableIDs.length; i++)
ids[i] = availableIDs[i];
return ids;
}
File d = new File(zoneinfo_dir);
ArrayList list = new ArrayList(30);
int count = getAvailableIDs(d, "", list) + aliases0.size();
availableIDs = new String[count];
String[] ids = new String[count];
count = 0;
for (int i = 0; i < list.size(); i++)
{
String[] s = (String[]) list.get(i);
for (int j = 0; j < s.length; j++)
if (s[j] != null)
{
availableIDs[count] = s[j];
ids[count++] = s[j];
}
}
Iterator iter = aliases0.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry = (Map.Entry) iter.next();
availableIDs[count] = (String) entry.getKey();
ids[count++] = (String) entry.getKey();
}
return ids;
}
}
/**