package er.extensions.components;
import java.util.Date;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOApplication;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.appserver.ERXSession;
import er.extensions.eof.ERXQ;
import er.extensions.eof.ERXS;
import er.extensions.foundation.ERXArrayUtilities;
/**
* This component adds javascript to a page to grab the system time zone
* from the browser and write the time zone to the <code>timeZone</code>
* attribute of the session via a call to the <code>setTimeZone()</code>
* method. The information is sent to the session using an ajax call.
* This code determines a time zone based on minutes offset from
* GMT, whether the time zone observes DST, and if DST, whether the time
* zone is in the northern or southern hemisphere. Since there may be more
* than one time zone that matches these values, the array of possible
* values is compared against an array of preferred values if one is
* supplied. If no preferred values are supplied, the zone selected is
* pulled from the list of possible options in no particular order. Use
* of an {@link ERXSession} is expected/required.
*
* @binding preferredTimeZones an array of preferred TimeZone objects. This
* array takes precedence over the preferredTimeZoneIDs binding.
* @binding preferredTimeZoneIDs an array of preferred TimeZone id strings
*
* @author Ramsey Gurley
*
*/
public class ERXTimeZoneDetector extends ERXStatelessComponent {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
/**
* Logger
*/
private static final Logger log = LoggerFactory.getLogger(ERXTimeZoneDetector.class);
public static final String TIMEZONE_SESSION_KEY = "detectedTimeZone";
private static final String TIMEZONE_DATA_KEY = "_timezone";
private static volatile NSArray<TimeZone> allZones;
public ERXTimeZoneDetector(WOContext context) {
super(context);
}
public static NSArray<TimeZone> allZones() {
if (allZones == null) {
synchronized (ERXTimeZoneDetector.class) {
if (allZones == null) {
String[] ids = TimeZone.getAvailableIDs();
NSMutableArray<TimeZone> tzs = new NSMutableArray<>(ids.length);
for (int i = 0; i < ids.length; i++) {
TimeZone tz = TimeZone.getTimeZone(ids[i]);
tzs.addObject(tz);
}
ERXS.sort(tzs, ERXS.asc("displayName"));
allZones = tzs.immutableClone();
}
}
}
return allZones;
}
public static NSArray<TimeZone> zonesWithRawOffset(int minutes, boolean dst, boolean southern) {
int rawOffset = minutes * 60 * 1000;
EOQualifier q = ERXQ.equals("rawOffset", rawOffset);
q = ERXQ.and(q, dst ? ERXQ.isTrue("useDaylightTime") : ERXQ.isFalse("useDaylightTime"));
NSArray<TimeZone> result = EOQualifier.filteredArrayWithQualifier(allZones(), q);
if (dst) {
Date d = new NSTimestamp(2010, southern ? 0 : 5, 1, 0, 0, 0, TimeZone.getTimeZone("GMT"));
NSMutableArray<TimeZone> tzs = new NSMutableArray<>();
for (TimeZone tz : result) {
if (tz.inDaylightTime(d)) {
tzs.addObject(tz);
}
}
result = tzs.immutableClone();
}
return result;
}
public TimeZone zoneWithRawOffset(int minutes, boolean dst, boolean southern) {
NSArray<TimeZone> zones = zonesWithRawOffset(minutes, dst, southern);
TimeZone tz = zones.firstObjectCommonWithArray(preferredTimeZones());
if(tz == null) { tz = ERXArrayUtilities.firstObject(zones); }
return tz;
}
/**
* Returns true if the component should include a script to post time zone
* data back to the server. This remains true until the time zone data is
* captured.
*
* @return true if ajax script should be included
*/
public boolean shouldPostData() {
ERXSession session = (ERXSession) context().session();
return !(session.objectStore().valueForKey(TIMEZONE_SESSION_KEY) instanceof String);
}
/**
* @return key used to identify timezone form value
*/
public String formValueKey() {
return TIMEZONE_DATA_KEY;
}
/**
* The ajax request URL for this component.
* @return the post URL for the ajax post request
*/
public String postURL() {
String key = WOApplication.application().ajaxRequestHandlerKey();
return context().componentActionURL(key);
}
/**
* Overridden to capture the time zone data being sent from the client.
*/
@Override
public void takeValuesFromRequest(WORequest request, WOContext context) {
super.takeValuesFromRequest(request, context);
if (shouldPostData() && request.formValueForKey(TIMEZONE_DATA_KEY) != null) {
ERXSession session = ERXSession.session();
String zoneString = request.stringFormValueForKey(TIMEZONE_DATA_KEY);
session.objectStore().takeValueForKey(zoneString, TIMEZONE_SESSION_KEY);
session.setJavaScriptEnabled(true);
String[] data = StringUtils.split(zoneString, ',');
int rawOffset = Integer.valueOf(data[0]).intValue();
boolean dst = "1".equals(data[1]);
boolean southern = "1".equals(data[2]);
TimeZone tz = zoneWithRawOffset(rawOffset, dst, southern);
// Call ERXSession.setTimeZone() if tz is not null
// https://github.com/wocommunity/wonder/issues/774
if (tz != null) {
session.setTimeZone(tz);
}
else {
log.warn("Unable to find a timezone for '{}'.", zoneString);
}
}
}
public NSArray<TimeZone> preferredTimeZones() {
NSArray<TimeZone> result;
result = (NSArray<TimeZone>)valueForBinding("preferredTimeZones");
if(result != null) {
return result;
}
NSArray<String> ids = (NSArray<String>)valueForBinding("preferredTimeZoneIDs");
if(ids == null) {
result = NSArray.emptyArray();
return result;
}
NSMutableArray<TimeZone> tzs = new NSMutableArray<>(ids.count());
for (int i = 0; i < ids.count(); i++) {
TimeZone tz = TimeZone.getTimeZone(ids.objectAtIndex(i));
tzs.addObject(tz);
}
result = tzs.immutableClone();
return result;
}
}