001    package org.apache.tapestry.scriptaculous;
002    
003    import org.apache.hivemind.ApplicationRuntimeException;
004    import org.apache.hivemind.util.Defense;
005    import org.apache.tapestry.*;
006    import org.apache.tapestry.coerce.ValueConverter;
007    import org.apache.tapestry.engine.DirectServiceParameter;
008    import org.apache.tapestry.engine.IEngineService;
009    import org.apache.tapestry.engine.ILink;
010    import org.apache.tapestry.form.AbstractFormComponent;
011    import org.apache.tapestry.form.TranslatedField;
012    import org.apache.tapestry.form.TranslatedFieldSupport;
013    import org.apache.tapestry.form.ValidatableFieldSupport;
014    import org.apache.tapestry.json.JSONObject;
015    import org.apache.tapestry.link.DirectLink;
016    import org.apache.tapestry.listener.ListenerInvoker;
017    import org.apache.tapestry.services.ResponseBuilder;
018    import org.apache.tapestry.util.SizeRestrictingIterator;
019    import org.apache.tapestry.valid.ValidatorException;
020    
021    import java.text.ParseException;
022    import java.util.*;
023    
024    /**
025     * Implementation of the <a href="http://wiki.script.aculo.us/scriptaculous/show/Ajax.Autocompleter">Ajax.Autocompleter</a> in
026     * the form of a {@link org.apache.tapestry.form.TextField} like component with the additional ability to dynamically suggest
027     * values via XHR requests.
028     *
029     * <p>
030     * This component will use the html element tag name defined in your html template to include it to determine whether or not
031     * to render a TextArea or TextField style input element. For example, specifying a component definition such as:
032     * </p>
033     *
034     * <pre>&lt;input jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
035     *
036     * <p>
037     * would render something looking like:
038     * </p>
039     *
040     * <pre>&lt;input type="text" name="suggest" id="suggest" autocomplete="off" value="literal:A default value" /&gt;</pre>
041     *
042     * <p>while a defintion of</p>
043     *
044     * <pre>&lt;textarea jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
045     *
046     * <p>would render something like:</p>
047     *
048     * <pre>
049     *  &lt;textarea name="suggest" id="suggest" &gt;A default value&lt;textarea/&gt;
050     * </pre>
051     *
052     */
053    public abstract class Suggest extends AbstractFormComponent implements TranslatedField, IDirect {
054    
055        /**
056         * Injected service used to invoke whatever listeners people have setup to handle
057         * changing value from this field.
058         *
059         * @return The invoker.
060         */
061        public abstract ListenerInvoker getListenerInvoker();
062    
063        /**
064         * Injected response builder for doing specific XHR things.
065         *
066         * @return ResponseBuilder for this request. 
067         */
068        public abstract ResponseBuilder getResponse();
069    
070        /**
071         * Associated javascript template.
072         *
073         * @return The script template.
074         */
075        public abstract IScript getScript();
076    
077        /**
078         * Used to convert form input values.
079         *
080         * @return The value converter to use.
081         */
082        public abstract ValueConverter getValueConverter();
083    
084        /**
085         * Injected.
086         *
087         * @return Service used to validate input.
088         */
089        public abstract ValidatableFieldSupport getValidatableFieldSupport();
090    
091        /**
092         * Injected.
093         *
094         * @return Translation service.
095         */
096        public abstract TranslatedFieldSupport getTranslatedFieldSupport();
097    
098        /**
099         * Injected.
100         *
101         * @return The {@link org.apache.tapestry.engine.DirectService} engine.  
102         */
103        public abstract IEngineService getEngineService();
104    
105        ////////////////////////////////////////////////////////
106        // Parameters
107        ////////////////////////////////////////////////////////
108    
109        public abstract Object getValue();
110        public abstract void setValue(Object value);
111    
112        public abstract ListItemRenderer getListItemRenderer();
113        public abstract void setListItemRenderer(ListItemRenderer renderer);
114    
115        public abstract IActionListener getListener();
116    
117        public abstract Object getListSource();
118        public abstract void setListSource(Object value);
119    
120        public abstract int getMaxResults();
121    
122        public abstract Object getParameters();
123    
124        public abstract String getOptions();
125    
126        public abstract String getUpdateElementClass();
127    
128        /**
129         * Used internally to track listener invoked searches versus
130         * normal rendering requests.
131         *
132         * @return True if search was triggered, false otherwise.
133         */
134        public abstract boolean isSearchTriggered();
135        public abstract void setSearchTriggered(boolean value);
136    
137        public boolean isRequired()
138        {
139            return getValidatableFieldSupport().isRequired(this);
140        }
141    
142        protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
143        {
144            // render search triggered response instead of normal render if
145            // listener was invoked
146    
147            IForm form = TapestryUtils.getForm(cycle, this);
148            setForm(form);
149    
150            if (form.wasPrerendered(writer, this))
151                return;
152    
153            if (!form.isRewinding() && !cycle.isRewinding()
154                && getResponse().isDynamic() && isSearchTriggered())
155            {
156                setName(form);
157    
158                // do nothing if it wasn't for this instance - such as in a loop
159    
160                if (cycle.getParameter(getClientId()) == null)
161                    return;
162    
163                renderList(writer, cycle);
164                return;
165            }
166    
167            // defer to super if normal render
168    
169            super.renderComponent(writer, cycle);
170        }
171    
172        /**
173         * Invoked only when a search has been triggered to render out the &lt;li&gt; list of
174         * dynamic suggestion options.
175         *
176         * @param writer
177         *          The markup writer.
178         * @param cycle
179         *          The associated request.
180         */
181        public void renderList(IMarkupWriter writer, IRequestCycle cycle)
182        {
183            Defense.notNull(getListSource(), "listSource for Suggest component.");
184    
185            Iterator values = (Iterator)getValueConverter().coerceValue(getListSource(), Iterator.class);
186    
187            if (isParameterBound("maxResults"))
188            {
189                values = new SizeRestrictingIterator(values, getMaxResults());
190            }
191    
192            getListItemRenderer().renderList(writer, cycle, values);
193        }
194    
195        protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
196        {
197            String value = getTranslatedFieldSupport().format(this, getValue());
198            boolean isTextArea = getTemplateTagName().equalsIgnoreCase("textarea");
199    
200            renderDelegatePrefix(writer, cycle);
201    
202            if (isTextArea)
203                writer.begin(getTemplateTagName());
204            else
205                writer.beginEmpty(getTemplateTagName());
206    
207            // only render input attributes if not a textarea
208            if (!isTextArea)
209            {
210                writer.attribute("type", "text");
211                writer.attribute("autocomplete", "off");
212            }
213    
214            renderIdAttribute(writer, cycle);
215            writer.attribute("name", getName());
216    
217            if (isDisabled())
218                writer.attribute("disabled", "disabled");
219    
220            renderInformalParameters(writer, cycle);
221            renderDelegateAttributes(writer, cycle);
222    
223            getTranslatedFieldSupport().renderContributions(this, writer, cycle);
224            getValidatableFieldSupport().renderContributions(this, writer, cycle);
225    
226            if (value != null)
227            {
228                if (!isTextArea)
229                    writer.attribute("value", value);
230                else
231                    writer.print(value);
232            }
233    
234            if (!isTextArea)
235                writer.closeTag();
236            else
237                writer.end();
238    
239            renderDelegateSuffix(writer, cycle);
240    
241            // render update element
242    
243            writer.begin("div");
244            writer.attribute("id", getClientId() + "choices");
245            writer.attribute("class", getUpdateElementClass());
246            writer.end();
247    
248            // render javascript
249    
250            JSONObject json = null;
251            String options = getOptions();
252    
253            try {
254    
255                json = options != null ? new JSONObject(options) : new JSONObject();
256    
257            } catch (ParseException ex)
258            {
259                throw new ApplicationRuntimeException(ScriptaculousMessages.invalidOptions(options, ex), this.getBinding("options").getLocation(), ex);
260            }
261    
262            // bind onFailure client side function if not already defined
263    
264            if (!json.has("onFailure"))
265            {
266                json.put("onFailure", "tapestry.error");
267            }
268    
269            if (!json.has("encoding"))
270            {
271                json.put("encoding", cycle.getEngine().getOutputEncoding());
272            }
273    
274            Map parms = new HashMap();
275            parms.put("inputId", getClientId());
276            parms.put("updateId", getClientId() + "choices");
277            parms.put("options", json.toString());
278    
279            Object[] specifiedParams = DirectLink.constructServiceParameters(getParameters());
280            Object[] listenerParams = null;
281            if (specifiedParams != null)
282            {
283                listenerParams = new Object[specifiedParams.length + 1];
284                System.arraycopy(specifiedParams, 0, listenerParams, 1, specifiedParams.length);
285            } else {
286    
287                listenerParams = new Object[1];
288            }
289    
290            listenerParams[0] = getClientId();
291    
292            ILink updateLink = getEngineService().getLink(isStateful(), new DirectServiceParameter(this, listenerParams));
293            parms.put("updateUrl", updateLink.getURL());
294    
295            PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this);
296            getScript().execute(this, cycle, pageRenderSupport, parms);
297        }
298    
299        /**
300         * Rewinds the component, doing translation, validation and binding.
301         */
302        protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
303        {
304            String value = cycle.getParameter(getName());
305            try
306            {
307                Object object = getTranslatedFieldSupport().parse(this, value);
308                getValidatableFieldSupport().validate(this, writer, cycle, object);
309    
310                setValue(object);
311            } catch (ValidatorException e)
312            {
313                getForm().getDelegate().recordFieldInputValue(value);
314                getForm().getDelegate().record(e);
315            }
316        }
317    
318        /**
319         * Triggers the listener. The parameters passed are the current text
320         * and those specified in the parameters parameter of the component.
321         */
322        public void trigger(IRequestCycle cycle)
323        {
324            IActionListener listener = getListener();
325            if (listener == null)
326                throw Tapestry.createRequiredParameterException(this, "listener");
327    
328            Object[] params = cycle.getListenerParameters();
329    
330            // replace the first param with the correct value
331            String inputId = (String)params[0];
332            params[0] = cycle.getParameter(inputId);
333    
334            cycle.setListenerParameters(params);
335    
336            setSearchTriggered(true);
337    
338            getListenerInvoker().invokeListener(listener, this, cycle);
339        }
340    
341        public List getUpdateComponents()
342        {
343            return Arrays.asList(new Object[] { getClientId() });
344        }
345    
346        public boolean isAsync()
347        {
348            return true;
349        }
350    
351        public boolean isJson()
352        {
353            return false;
354        }
355    
356        /**
357         * Sets the default {@link ListItemRenderer} for component, to be overriden as
358         * necessary by component parameters.
359         */
360        protected void finishLoad()
361        {
362            setListItemRenderer(DefaultListItemRenderer.SHARED_INSTANCE);
363        }
364    }