/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.trinidadinternal.renderkit.core.xhtml;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TimeZone;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.convert.DateTimeConverter;
import javax.faces.validator.Validator;
import org.apache.myfaces.trinidad.bean.FacesBean;
import org.apache.myfaces.trinidad.bean.PropertyKey;
import org.apache.myfaces.trinidad.component.core.input.CoreInputDate;
import org.apache.myfaces.trinidad.context.RenderingContext;
import org.apache.myfaces.trinidad.context.RequestContext;
import org.apache.myfaces.trinidad.event.ReturnEvent;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.render.RenderUtils;
import org.apache.myfaces.trinidad.validator.DateTimeRangeValidator;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent;
import org.apache.myfaces.trinidadinternal.convert.GenericConverterFactory;
import org.apache.myfaces.trinidadinternal.renderkit.core.CoreRenderKit;
import org.apache.myfaces.trinidadinternal.renderkit.core.pages.GenericEntry;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.jsLibs.ConfigurationScriptlet;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.jsLibs.DialogStyleScriptlet;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.jsLibs.Scriptlet;
/**
*/
public class SimpleInputDateRenderer
extends SimpleInputListOfValuesRenderer
{
public SimpleInputDateRenderer()
{
this(CoreInputDate.TYPE);
}
public SimpleInputDateRenderer(FacesBean.Type type)
{
super(type);
}
@Override
protected void findTypeConstants(FacesBean.Type type)
{
super.findTypeConstants(type);
_chooseIdKey = type.findKey("chooseId");
}
@Override
protected void queueActionEvent(FacesContext context, UIComponent component)
{
FacesBean bean = getFacesBean(component);
// If there's a non-default action, then just launch away
if (getAction(bean) != null)
{
super.queueActionEvent(context, component);
}
// Otherwise, we'll fall back to launching the default dialog
// (This should only happen on devices without support for
// custom windows - everything else would have just launched
// a calendar window with the _ldp JS function)
else
{
Object submittedValue = getSubmittedValue(bean);
Date date = null;
try
{
Object converted = getConvertedValue(context,
component,
submittedValue);
if (converted instanceof Date)
date = (Date) converted;
else
{
GenericConverterFactory fac = GenericConverterFactory.getCurrentInstance();
if (fac.isConvertible(converted, Date.class))
date = (Date) fac.convert(converted, Date.class);
}
}
// Not a big deal; just means that an invalid value was entered,
// so we'll launch the dialog showing nothing
catch (ConverterException ce)
{
_LOG.fine(ce);
}
RequestContext afContext = RequestContext.getCurrentInstance();
DateTimeRangeValidator dtrv = _findDateTimeRangeValidator(bean);
if (date == null)
date = new Date();
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put(XhtmlConstants.VALUE_PARAM, _getDateAsString(date));
parameters.put(XhtmlConstants.MIN_VALUE_PARAM,
dtrv == null
? null : _getDateAsString(dtrv.getMinimum()));
parameters.put(XhtmlConstants.MAX_VALUE_PARAM,
dtrv == null
? null : _getDateAsString(dtrv.getMaximum()));
parameters.put(GenericEntry.getEntryKeyParam(),
GenericEntry.CALENDAR_DIALOG_ENTRY);
afContext.launchDialog(GenericEntry.getGenericEntryViewRoot(context),
parameters,
component,
true,
null);
}
}
/**
* Give subclasses a chance to override the ReturnEvent.
*/
@Override
protected void queueReturnEvent(
FacesContext context,
UIComponent component,
ReturnEvent event)
{
Object returnValue = event.getReturnValue();
GenericConverterFactory fac = GenericConverterFactory.getCurrentInstance();
// If we got passed a Date object, send it back to String
// land (where it needs to be for submitted values).
if ((returnValue instanceof Date) || fac.isConvertible(returnValue, Date.class))
{
FacesBean bean = getFacesBean(component);
Converter converter = getConverter(bean);
if (converter == null)
converter = getDefaultConverter(context, bean);
if (converter != null)
{
returnValue = converter.getAsString(context,
component,
returnValue);
}
else
{
returnValue = returnValue.toString();
}
event = new ReturnEvent(component,
returnValue,
event.getReturnParameters());
}
event.queue();
}
@Override
protected void encodeAllAsElement(
FacesContext context,
RenderingContext arc,
UIComponent component,
FacesBean bean) throws IOException
{
if (!_dateScriptletsRegistered)
{
// Register our scriptlet
DateTimeZoneOffsetScriptlet.sharedInstance().registerSelf();
_dateScriptletsRegistered = true;
}
String chooseId = _computeChooseId(context, component, bean);
arc.getProperties().put(_CACHED_CHOOSE_ID, chooseId);
// Add the scriptlets required by the date field
// =-=AEW What's this one?
XhtmlUtils.addLib(context, arc, "_dfsv()");
XhtmlUtils.addLib(context, arc, "_fixDFF()");
super.encodeAllAsElement(context, arc, component, bean);
if (!getDisabled(bean))
{
_checkIfActive(context, arc, component, _getChooseId(arc));
}
}
@Override
protected void renderIcon(
FacesContext context,
RenderingContext arc,
UIComponent component,
FacesBean bean) throws IOException
{
// Add the two scriptlets required by the icon
XhtmlUtils.addLib(context,
arc,
ConfigurationScriptlet.sharedInstance().getScriptletKey());
XhtmlUtils.addLib(context,
arc,
_DATE_TIME_ZONE_OFFSET_KEY);
// Add the dialog styles so the picker window is skinned correctly
if (CoreRenderKit.usePopupForDialog(context, RequestContext.getCurrentInstance()))
{
XhtmlUtils.addLib(context,
arc,
DialogStyleScriptlet.sharedInstance().getScriptletKey());
}
super.renderIcon(context, arc, component, bean);
}
@Override
protected void renderAfterTextField(
FacesContext context,
RenderingContext arc,
UIComponent component,
FacesBean bean) throws IOException
{
// ADFFACES-317: don't bother rendering the icon if there's
// an attached chooser
if (_getChooseId(arc) == null)
super.renderAfterTextField(context, arc, component, bean);
}
/**
* @todo - should the default style be "short" or "default", which
* may map to "medium"
* @todo medium / default is what it is defaulted to in faces
*/
@Override
protected Converter getDefaultConverter(
FacesContext context,
FacesBean bean)
{
Converter converter = context.getApplication().
createConverter(DateTimeConverter.CONVERTER_ID);
// for convenience, we will set the time zone of the converter to that
// specified by the context or, if that is not present, to the time zone
// of the server
if (converter instanceof DateTimeConverter)
{
DateTimeConverter dtc = (DateTimeConverter) converter;
boolean trinidadDTC = _isTrinidadDateTimeConverter(converter);
if (!trinidadDTC)
{
// if it is not the Trinidad DateTimeConverter, set the date style to
// short
dtc.setDateStyle("short");
}
// if it is not the Trinidad DateTimeConverter or (it is AND
// no time zone is set) then we want to set the
// time zone to the one in the faces context or use
// the default server time zone on the converter
if (!trinidadDTC || dtc.getTimeZone() == null)
{
TimeZone tz = null;
RequestContext requestContext = RequestContext.getCurrentInstance();
tz = requestContext.getTimeZone();
if(tz == null)
{
tz = TimeZone.getDefault();
}
dtc.setTimeZone(tz);
}
}
return converter;
}
@Override
protected String getOnblur(FacesBean bean)
{
String onblur = super.getOnblur(bean);
RenderingContext arc = RenderingContext.getCurrentInstance();
String chooseId = _getChooseId(arc);
int length = _BLUR_PREFIX.length() + 4;
if (chooseId != null)
length += chooseId.length();
StringBuilder buffer = new StringBuilder(length);
buffer.append(_BLUR_PREFIX);
if (chooseId != null)
{
buffer.append(",'");
buffer.append(chooseId);
buffer.append("'");
}
buffer.append(")");
return XhtmlUtils.getChainedJS(buffer.toString(), onblur, false);
}
@Override
protected String getOnfocus(FacesBean bean)
{
String onfocus = super.getOnfocus(bean);
RenderingContext arc = RenderingContext.getCurrentInstance();
String chooseId = _getChooseId(arc);
// The special _dff handler is only needed for date fields
// connected to a chooser; the blur handler is needed all the time.
if (chooseId != null)
{
int length = _FOCUS_PREFIX.length() + 4;
length += chooseId.length();
StringBuilder buffer = new StringBuilder(length);
buffer.append(_FOCUS_PREFIX);
buffer.append(",'");
buffer.append(chooseId);
buffer.append("'");
buffer.append(")");
return XhtmlUtils.getChainedJS(buffer.toString(), onfocus, false);
}
else
{
return onfocus;
}
}
@Override
protected String getLaunchOnclick(
FacesContext context,
RenderingContext arc,
UIComponent component,
FacesBean bean) throws IOException
{
// If the field has an action, use the default behavior. Or,
// if the field doesn't support launching a window at all,
// use the default behavior.
if ((getAction(bean) != null) ||
!Boolean.TRUE.equals(
arc.getAgent().getCapabilities().get(TrinidadAgent.CAP_MULTIPLE_WINDOWS)))
return super.getLaunchOnclick(context, arc, component, bean);
String id = arc.getCurrentClientId();
if ((id == null) || (arc.getFormData() == null))
return null;
// we want something big enough
StringBuilder onClickBuffer = new StringBuilder(100);
onClickBuffer.append("_ldp('");
onClickBuffer.append(arc.getFormData().getName());
onClickBuffer.append("','");
onClickBuffer.append(id);
onClickBuffer.append('\'');
// Decide if we should display the picker in a popup or window
onClickBuffer.append(',');
onClickBuffer.append(CoreRenderKit.usePopupForDialog(context,
RequestContext.getCurrentInstance()));
DateTimeRangeValidator dtrv = _findDateTimeRangeValidator(bean);
if (dtrv != null)
{
String minTime = _getDateAsString(dtrv.getMinimum());
String maxTime = _getDateAsString(dtrv.getMaximum());
if ((minTime != null) || (maxTime != null))
{
onClickBuffer.append(',');
if (minTime != null)
{
onClickBuffer.append(minTime);
}
else
{
// placeholder for next parameters
onClickBuffer.append("(void 0)");
}
}
if (maxTime != null)
{
onClickBuffer.append(',');
onClickBuffer.append(maxTime);
}
}
onClickBuffer.append("); return false");
return onClickBuffer.toString();
}
@Override
protected Integer getDefaultColumns(RenderingContext arc, FacesBean bean)
{
Integer columnsInteger = null;
Converter converter = getConverter(bean);
// Ignoring the "default" converter code is intentional; we'll just
// fall through to _DEFAULT_COLUMNS here to save time
if (converter instanceof
org.apache.myfaces.trinidadinternal.convert.DateTimeConverter)
{
int columns = ((org.apache.myfaces.trinidadinternal.convert.DateTimeConverter)
converter).getColumns();
columnsInteger = (columns);
}
else
{
columnsInteger = _DEFAULT_COLUMNS;
}
return columnsInteger;
}
@Override
protected String getButtonIconName()
{
return SkinSelectors.AF_SELECT_INPUT_DATE_LAUNCH_ICON_NAME;
}
protected String getChooseId(FacesBean bean)
{
return toString(bean.getProperty(_chooseIdKey));
}
@Override
protected String getSearchDesc(FacesBean bean)
{
RenderingContext arc = RenderingContext.getCurrentInstance();
if (isInaccessibleMode(arc))
return null;
return arc.getTranslatedString(_LAUNCH_PICKER_TIP_KEY);
}
@Override
protected String getRootStyleClass(FacesBean bean)
{
return "af|inputDate";
}
@Override
protected String getContentStyleClass(FacesBean bean)
{
return "af|inputDate::content";
}
private String _getChooseId(RenderingContext arc)
{
return (String) arc.getProperties().get(_CACHED_CHOOSE_ID);
}
private String _computeChooseId(
FacesContext context,
UIComponent component,
FacesBean bean)
{
return RenderUtils.getRelativeId(context,
component,
getChooseId(bean));
}
// Checks to see whether the current dateField should
// be active, and if so, renders a script that will activate
// it.
@SuppressWarnings("unchecked")
private void _checkIfActive(
FacesContext context,
RenderingContext arc,
UIComponent component,
String chooseId
) throws IOException
{
if (chooseId == null)
return;
String id = getClientId(context, component);
Map<String, Boolean> activeDateFields = (Map<String, Boolean>)
arc.getProperties().get(_ACTIVE_DATE_FIELDS_KEY);
if (activeDateFields == null)
{
activeDateFields = new HashMap<String, Boolean>();
arc.getProperties().put(_ACTIVE_DATE_FIELDS_KEY, activeDateFields);
}
// The first dateField that is rendered for each inlineDatePicker
// is the "active" dateField. Check to see if we already
// have an active dateField for this inlineDatePicker.
if (activeDateFields.get(chooseId) == null)
{
// We don't already have an active dateField, so
// this one is it. Render the script to activate
// the dateField.
ResponseWriter writer = context.getResponseWriter();
writer.startElement("script", component);
renderScriptTypeAttribute(context, arc);
renderScriptDeferAttribute(context, arc);
writer.writeText("_dfa('", null);
writer.writeText(id, null);
writer.writeText("','", null);
writer.writeText(chooseId, null);
writer.writeText("');", null);
writer.endElement("script");
// Mark the inlineDatePicker as having an active dateField
activeDateFields.put(chooseId, Boolean.TRUE);
}
}
//
// Find a DateTimeRangeValidator for use in setting up a
// minimum and maximum value
//
private DateTimeRangeValidator _findDateTimeRangeValidator(FacesBean bean)
{
Iterator<Validator> validators = getValidators(bean);
while (validators.hasNext())
{
Object validator = validators.next();
if (validator instanceof DateTimeRangeValidator)
return (DateTimeRangeValidator) validator;
}
return null;
}
private static boolean _isTrinidadDateTimeConverter(Converter converter)
{
return (converter instanceof
org.apache.myfaces.trinidad.convert.DateTimeConverter);
}
/**
* Stringify the date into canonical form; we currently
* use the long integer date.getTime().
*/
private static String _getDateAsString(Date date)
{
if (date == null)
return null;
return String.valueOf(_adjustTimeZone(date));
}
/**
* Adjust the specified date, which is in server timeZone to the timeZone
* found in RequestContext and return the new date long value.
*/
@SuppressWarnings("cast")
private static long _adjustTimeZone(Date date)
{
// get the current date of the server
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
long dateValueInMs = calendar.getTimeInMillis();
// adjust the date; first, get this into GMT
long tzOffset = calendar.get(Calendar.ZONE_OFFSET) +
calendar.get(Calendar.DST_OFFSET);
// get the timeZone specified in trinidad-config, if any or the
// client timeZone and find out the difference in timeZone
TimeZone timeZone = RequestContext.getCurrentInstance().getTimeZone();
if(timeZone == null)
{
timeZone = TimeZone.getDefault();
}
// then, adjust for the "local" time zone (either the client's, as
// specified in RequestContext, or the server's if it wasn't specified
// in RequestContext)
tzOffset -= timeZone.getOffset(dateValueInMs);
// make sure that adjusting to correct timeZone doesn't take the
// long value out of the range. Calendar too doesn't handle this
// properly ie. MIN_VALUE < (longValue + tzOffset) < MAX_VALUE.
if (tzOffset < 0)
{
// Cast to (float) has a purpose
tzOffset = (long)Math.max((float)tzOffset,
(float)Long.MIN_VALUE - (float)dateValueInMs);
}
else
{
// Cast to (float) has a purpose
tzOffset = (long)Math.min((float)tzOffset,
(float)Long.MAX_VALUE - (float)dateValueInMs);
}
// adjust the date in ms to the adjusted time zone.
long adjusted = dateValueInMs + tzOffset;
return adjusted;
}
// this scriptlet is to pass in the time zone raw offset from the locale
// context in javascript. It will be used in the date picker when
// we format the date field with time values.
private static class DateTimeZoneOffsetScriptlet extends Scriptlet
{
static public Scriptlet sharedInstance()
{
return _sInstance;
}
private DateTimeZoneOffsetScriptlet()
{
}
@Override
public Object getScriptletKey()
{
return _DATE_TIME_ZONE_OFFSET_KEY;
}
@Override
protected void outputScriptletContent(
FacesContext context,
RenderingContext arc)
throws IOException
{
// get the tzOffset for the current date. I will compare this with
// the tzOffset for the current data in javascript, and use it to
// manipulate the formatted datefield's value so that it shows the
// localeContext's timeZone, and not the time zone on the browser.
TimeZone tz = arc.getLocaleContext().getTimeZone();
int tzOffsetMinutes = tz.getOffset(System.currentTimeMillis())/(1000*60);
ResponseWriter writer = context.getResponseWriter();
writer.writeText("var _uixLocaleTZ=", null);
writer.writeText(String.valueOf(tzOffsetMinutes), null);
writer.writeText(";", null);
}
private static final Scriptlet _sInstance =
new DateTimeZoneOffsetScriptlet();
}
private PropertyKey _chooseIdKey;
private static final Integer _DEFAULT_COLUMNS = Integer.valueOf(10);
private static final String _BLUR_PREFIX = "_dfb(this";
private static final String _FOCUS_PREFIX = "_dff(this";
// RenderingContext property key for the Map which tracks the
// active date field for each inlineDatePicker
private static final Object _ACTIVE_DATE_FIELDS_KEY = new Object();
private static final String _LAUNCH_PICKER_TIP_KEY =
"af_inputDate.LAUNCH_PICKER_TIP";
// Key for remembering the cached chooseId
private static final Object _CACHED_CHOOSE_ID = new Object();
// name for our scriptlet
private static final String _DATE_TIME_ZONE_OFFSET_KEY = "dateTimeZoneOffset";
private static boolean _dateScriptletsRegistered = false;
private static final TrinidadLogger _LOG =
TrinidadLogger.createTrinidadLogger(SimpleInputDateRenderer.class);
}