package pt.ist.renderers.extensions; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang.StringUtils; import pt.ist.fenixframework.DomainObject; import pt.ist.renderers.InputRenderer; import pt.ist.renderers.components.HtmlBlockContainer; import pt.ist.renderers.components.HtmlComponent; import pt.ist.renderers.components.HtmlContainer; import pt.ist.renderers.components.HtmlHiddenField; import pt.ist.renderers.components.HtmlLink; import pt.ist.renderers.components.HtmlScript; import pt.ist.renderers.components.HtmlSimpleValueComponent; import pt.ist.renderers.components.HtmlText; import pt.ist.renderers.components.HtmlTextInput; import pt.ist.renderers.components.controllers.HtmlController; import pt.ist.renderers.components.converters.Converter; import pt.ist.renderers.components.state.IViewState; import pt.ist.renderers.extensions.converters.DomainObjectKeyConverter; import pt.ist.renderers.layouts.Layout; import pt.ist.renderers.model.MetaSlot; import pt.ist.renderers.model.MetaSlotKey; import pt.ist.renderers.utils.RenderUtils; import pt.ist.renderers.utils.RendererPropertyUtils; /** * This renderer allows you to search for a domain object by providing a list of * possible completions, for the text typed, using a javascript technique. * *
* This renderer can be used to do the input of any domain object because you * can configure the concrete service that searches the objects and all objects * are referred to by their internal id. It's recommended that a specialized * service is created for most cases so that it's as efficient as possible. * *
* Example:
*
* If the renderer is used to edit the slot slotA
of an object
* A and this property is set the value slotB
then when the
* field is submited the renderer will set the value of the text field in
* the slotB
of the object A.
*
*
* When you type in the text field an auto completion list is presented.
* Nevertheless an object is only selected when the user selects one element
* from the sugested completions. This means that if the user does not
* select one element after typing some text the value of the slot beeing
* edited will be set to null
.
*
* @property
*/
public void setRawSlotName(String rawSlotName) {
this.rawSlotName = rawSlotName;
}
public String getLabelField() {
return this.labelField;
}
/**
* This property allows you tho choose the name of the slot that will be
* used as the presentation of the object. If this proprty has the value
* slotL
then the list of suggestions will be a list of values
* obtained by invoking getSlotL
in each object.
*
* @property
*/
public void setLabelField(String labelField) {
this.labelField = labelField;
}
public String getFormat() {
return this.format;
}
/**
* Allows you select the presentation format. If not set the value of the
* field given by {@link #setLabelField(String) labelField} is used. See
* {@link pt.ist.renderers.utils.RenderUtils#getFormattedProperties(String, Object)}
* to see the accepted format syntax.
*
* @property
*/
public void setFormat(String format) {
this.format = format;
}
public Integer getMaxCount() {
return this.maxCount;
}
/**
* Limits the number of results that the servlet returns thus the number of
* suggestions given to the user.
*
* @property
*/
public void setMaxCount(Integer maxCount) {
this.maxCount = maxCount;
}
public String getAutoCompleteItemsStyleClass() {
return this.autoCompleteItemsStyleClass;
}
/**
* The html class of results shown.
*
* @property
*/
public void setAutoCompleteItemsStyleClass(String autoCompleteItemsStyleClass) {
this.autoCompleteItemsStyleClass = autoCompleteItemsStyleClass;
}
public String getAutoCompleteStyleClass() {
return this.autoCompleteStyleClass;
}
/**
* The html class of the container of the results shown.
*
* @property
*/
public void setAutoCompleteStyleClass(String autoCompleteStyleClass) {
this.autoCompleteStyleClass = autoCompleteStyleClass;
}
public String getTextFieldStyleClass() {
return this.textFieldStyleClass;
}
/**
* The html class of the text field.
*
* @property
*/
public void setTextFieldStyleClass(String textFieldStyleClass) {
this.textFieldStyleClass = textFieldStyleClass;
}
public String getErrorStyleClass() {
return this.errorStyleClass;
}
/**
* The html class of the error message.
*
* @property
*/
public void setErrorStyleClass(String errorStyleClass) {
this.errorStyleClass = errorStyleClass;
}
public String getSize() {
return this.size;
}
/**
* The size of the text field.
*
* @property
*/
public void setSize(String size) {
this.size = size;
}
public int getMinChars() {
return this.minChars;
}
/**
* The number of characters the user is required to type before any
* suggestion is offered.
*
* @property
*/
public void setMinChars(int minChars) {
this.minChars = minChars;
}
@Override
protected Layout getLayout(Object object, Class type) {
return new AutoCompleteLayout();
}
protected Converter createConverter() {
return new AutoCompleteConverter();
}
protected class AutoCompleteLayout extends Layout {
public AutoCompleteLayout() {
}
@Override
public HtmlComponent createComponent(Object object, Class type) {
HtmlBlockContainer container = new HtmlBlockContainer();
addScripts(container);
MetaSlotKey key = (MetaSlotKey) getContext().getMetaObject().getKey();
HtmlHiddenField valueField = new HtmlHiddenField();
valueField.setTargetSlot(key);
valueField.setId(key.toString() + "_AutoComplete");
valueField.setName(valueField.getId());
container.addChild(valueField);
if (object != null) {
Object oid = RendererPropertyUtils.getProperty(object, getValueField(), false);
valueField.setValue(oid == null ? null : oid.toString());
}
valueField.setConverter(createConverter());
HtmlHiddenField oldValueField = new HtmlHiddenField();
oldValueField.setId(key.toString() + "_OldValue");
oldValueField.setName(oldValueField.getId());
container.addChild(oldValueField);
HtmlTextInput textField = new HtmlTextInput();
textField.setId(key.toString());
textField.setName(textField.getId());
textField.setClasses(getTextFieldStyleClass());
textField.setSize(getSize());
textField.setAttribute("autocomplete", "off");
container.addChild(textField);
if (object != null && getLabelField() != null) {
String label = (String) RendererPropertyUtils.getProperty(object, getLabelField(), false);
textField.setValue(label);
} else if (getRawSlotName() != null) {
Object beanObject = getInputContext().getParentContext().getMetaObject().getObject();
if (beanObject != null) { // protect from a creation context
String rawText = (String) RendererPropertyUtils.getProperty(beanObject, getRawSlotName(), false);
textField.setValue(rawText);
if (rawText != null) {
valueField.setValue(TYPING_VALUE);
}
}
}
if (getRawSlotName() != null) {
textField.setController(new UpdateRawNameController(getRawSlotName()));
}
HtmlText errorMessage = new HtmlText(RenderUtils.getResourceString("fenix.renderers.autocomplete.error"));
errorMessage.setId(key.toString() + "_Error");
errorMessage.setClasses(getErrorStyleClass());
errorMessage.setStyle("display: none;");
container.addChild(errorMessage);
addFinalScript(container, textField.getId());
return container;
}
protected void addScripts(HtmlContainer container) {
HtmlLink link = new HtmlLink();
link.setModuleRelative(false);
link.setContextRelative(true);
String[] scriptNames = new String[] { "autoComplete.js", "autoCompleteHandlers.js" };
for (String script : scriptNames) {
addSingleScript(container, link, script);
}
}
protected void addSingleScript(HtmlContainer container, HtmlLink link, String scriptName) {
link.setUrl("/javaScript/" + scriptName);
HtmlScript script = new HtmlScript("text/javascript", link.calculateUrl(), true);
container.addChild(script);
}
protected void addFinalScript(HtmlContainer container, String textFieldId) {
HtmlLink link = new HtmlLink();
link.setModuleRelative(false);
link.setContextRelative(true);
link.setUrl(SERVLET_URI);
link.setParameter("args", getFormatedArgs());
link.setParameter("labelField", getLabelField());
link.setParameter("valueField", getValueField()); // TODO: allow
// configuration,1
// needs also
// converter
link.setParameter("styleClass", getAutoCompleteItemsStyleClass() == null ? "" : getAutoCompleteItemsStyleClass());
link.setEscapeAmpersand(false);
if (getFormat() != null) {
link.setParameter("format", getFormat());
}
if (getMaxCount() != null) {
link.setParameter("maxCount", String.valueOf(getMaxCount()));
}
String finalUri = link.calculateUrl();
String escapeId = escapeId(textFieldId);
String scriptText = "jQuery(\"input#"
+ escapeId
+ "\").autocomplete(\""
+ finalUri
+ "\", { minChars: "
+ getMinChars()
+ (!StringUtils.isEmpty(getAutoCompleteWidth()) ? ", width: '" + getAutoCompleteWidth() + "'" : "")
+ ", validSelection: false"
+ ", cleanSelection: clearAutoComplete, select: selectElement, after: updateCustomValue, error:showError}); +\n"
+ "jQuery(\"input[name='" + escapeId + "']\").val(jQuery(\"input#" + escapeId + "\").val());";
//on submit let's call the updateCustomValue to make sure that the rawSlotName is correctly filled;
if (getRawSlotName() != null) {
scriptText = scriptText.concat("\njQuery(\"input[name='" + escapeId + "']\").closest('form').submit(function() {\n" +
"var inputFieldVal = jQuery(\"input[name='" + escapeId + "_text']\").val()\n" +
"updateRawSlotNameOnSubmit(jQuery(\"input[name='" + escapeId
+ "_text']\"),inputFieldVal);});");
}
HtmlScript script = new HtmlScript();
script.setContentType("text/javascript");
script.setScript(scriptText);
container.addChild(script);
}
protected String escapeId(String textFieldId) {
return textFieldId.replace(".", "\\\\.").replaceAll(":", "\\\\\\\\:");
}
protected String getFormatedArgs() {
Object object = ((MetaSlot) getInputContext().getMetaObject()).getMetaObject().getObject();
return RenderUtils.getFormattedProperties(getArgs(), object);
}
}
protected static class UpdateRawNameController extends HtmlController {
private final String rawSlotName;
public UpdateRawNameController(String rawSlotName) {
super();
this.rawSlotName = rawSlotName;
}
@Override
public void execute(IViewState viewState) {
HtmlSimpleValueComponent component = (HtmlSimpleValueComponent) getControlledComponent();
updatRawSlot(viewState, component.getValue());
}
private void updatRawSlot(IViewState viewState, String value) {
Object object = viewState.getMetaObject().getObject();
try {
PropertyUtils.setProperty(object, this.rawSlotName, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
protected static class AutoCompleteConverter extends Converter {
public AutoCompleteConverter() {
super();
}
@Override
public Object convert(Class type, Object value) {
if (value == null || "".equals(value)) {
return null;
}
String text = (String) value;
if (text.equals(TYPING_VALUE)) {
return null;
}
String key = text;
return new DomainObjectKeyConverter().convert(type, key);
}
}
}