Coverage Report - org.apache.tapestry.scriptaculous.Suggest
 
Classes in this File Line Coverage Branch Coverage Complexity
Suggest
72% 
94% 
1.7
 
 1  
 package org.apache.tapestry.scriptaculous;
 2  
 
 3  
 import org.apache.hivemind.ApplicationRuntimeException;
 4  
 import org.apache.hivemind.util.Defense;
 5  
 import org.apache.tapestry.*;
 6  
 import org.apache.tapestry.coerce.ValueConverter;
 7  
 import org.apache.tapestry.engine.DirectServiceParameter;
 8  
 import org.apache.tapestry.engine.IEngineService;
 9  
 import org.apache.tapestry.engine.ILink;
 10  
 import org.apache.tapestry.form.AbstractFormComponent;
 11  
 import org.apache.tapestry.form.TranslatedField;
 12  
 import org.apache.tapestry.form.TranslatedFieldSupport;
 13  
 import org.apache.tapestry.form.ValidatableFieldSupport;
 14  
 import org.apache.tapestry.json.JSONObject;
 15  
 import org.apache.tapestry.link.DirectLink;
 16  
 import org.apache.tapestry.listener.ListenerInvoker;
 17  
 import org.apache.tapestry.services.ResponseBuilder;
 18  
 import org.apache.tapestry.util.SizeRestrictingIterator;
 19  
 import org.apache.tapestry.valid.ValidatorException;
 20  
 
 21  
 import java.text.ParseException;
 22  
 import java.util.*;
 23  
 
 24  
 /**
 25  
  * Implementation of the <a href="http://wiki.script.aculo.us/scriptaculous/show/Ajax.Autocompleter">Ajax.Autocompleter</a> in
 26  
  * the form of a {@link org.apache.tapestry.form.TextField} like component with the additional ability to dynamically suggest
 27  
  * values via XHR requests.
 28  
  *
 29  
  * <p>
 30  
  * This component will use the html element tag name defined in your html template to include it to determine whether or not
 31  
  * to render a TextArea or TextField style input element. For example, specifying a component definition such as:
 32  
  * </p>
 33  
  *
 34  
  * <pre>&lt;input jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
 35  
  *
 36  
  * <p>
 37  
  * would render something looking like:
 38  
  * </p>
 39  
  *
 40  
  * <pre>&lt;input type="text" name="suggest" id="suggest" autocomplete="off" value="literal:A default value" /&gt;</pre>
 41  
  *
 42  
  * <p>while a defintion of</p>
 43  
  *
 44  
  * <pre>&lt;textarea jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
 45  
  *
 46  
  * <p>would render something like:</p>
 47  
  *
 48  
  * <pre>
 49  
  *  &lt;textarea name="suggest" id="suggest" &gt;A default value&lt;textarea/&gt;
 50  
  * </pre>
 51  
  *
 52  
  */
 53  4
 public abstract class Suggest extends AbstractFormComponent implements TranslatedField, IDirect {
 54  
 
 55  
     /**
 56  
      * Injected service used to invoke whatever listeners people have setup to handle
 57  
      * changing value from this field.
 58  
      *
 59  
      * @return The invoker.
 60  
      */
 61  
     public abstract ListenerInvoker getListenerInvoker();
 62  
 
 63  
     /**
 64  
      * Injected response builder for doing specific XHR things.
 65  
      *
 66  
      * @return ResponseBuilder for this request. 
 67  
      */
 68  
     public abstract ResponseBuilder getResponse();
 69  
 
 70  
     /**
 71  
      * Associated javascript template.
 72  
      *
 73  
      * @return The script template.
 74  
      */
 75  
     public abstract IScript getScript();
 76  
 
 77  
     /**
 78  
      * Used to convert form input values.
 79  
      *
 80  
      * @return The value converter to use.
 81  
      */
 82  
     public abstract ValueConverter getValueConverter();
 83  
 
 84  
     /**
 85  
      * Injected.
 86  
      *
 87  
      * @return Service used to validate input.
 88  
      */
 89  
     public abstract ValidatableFieldSupport getValidatableFieldSupport();
 90  
 
 91  
     /**
 92  
      * Injected.
 93  
      *
 94  
      * @return Translation service.
 95  
      */
 96  
     public abstract TranslatedFieldSupport getTranslatedFieldSupport();
 97  
 
 98  
     /**
 99  
      * 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  0
         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  4
         IForm form = TapestryUtils.getForm(cycle, this);
 148  4
         setForm(form);
 149  
 
 150  4
         if (form.wasPrerendered(writer, this))
 151  0
             return;
 152  
 
 153  4
         if (!form.isRewinding() && !cycle.isRewinding()
 154  
             && getResponse().isDynamic() && isSearchTriggered())
 155  
         {
 156  1
             setName(form);
 157  
 
 158  
             // do nothing if it wasn't for this instance - such as in a loop
 159  
 
 160  1
             if (cycle.getParameter(getClientId()) == null)
 161  0
                 return;
 162  
 
 163  1
             renderList(writer, cycle);
 164  1
             return;
 165  
         }
 166  
 
 167  
         // defer to super if normal render
 168  
 
 169  3
         super.renderComponent(writer, cycle);
 170  2
     }
 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  1
         Defense.notNull(getListSource(), "listSource for Suggest component.");
 184  
 
 185  2
         Iterator values = (Iterator)getValueConverter().coerceValue(getListSource(), Iterator.class);
 186  
 
 187  1
         if (isParameterBound("maxResults"))
 188  
         {
 189  0
             values = new SizeRestrictingIterator(values, getMaxResults());
 190  
         }
 191  
 
 192  1
         getListItemRenderer().renderList(writer, cycle, values);
 193  1
     }
 194  
 
 195  
     protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
 196  
     {
 197  3
         String value = getTranslatedFieldSupport().format(this, getValue());
 198  3
         boolean isTextArea = getTemplateTagName().equalsIgnoreCase("textarea");
 199  
 
 200  3
         renderDelegatePrefix(writer, cycle);
 201  
 
 202  3
         if (isTextArea)
 203  1
             writer.begin(getTemplateTagName());
 204  
         else
 205  2
             writer.beginEmpty(getTemplateTagName());
 206  
 
 207  
         // only render input attributes if not a textarea
 208  3
         if (!isTextArea)
 209  
         {
 210  2
             writer.attribute("type", "text");
 211  2
             writer.attribute("autocomplete", "off");
 212  
         }
 213  
 
 214  3
         renderIdAttribute(writer, cycle);
 215  3
         writer.attribute("name", getName());
 216  
 
 217  3
         if (isDisabled())
 218  0
             writer.attribute("disabled", "disabled");
 219  
 
 220  3
         renderInformalParameters(writer, cycle);
 221  3
         renderDelegateAttributes(writer, cycle);
 222  
 
 223  3
         getTranslatedFieldSupport().renderContributions(this, writer, cycle);
 224  3
         getValidatableFieldSupport().renderContributions(this, writer, cycle);
 225  
 
 226  3
         if (value != null)
 227  
         {
 228  3
             if (!isTextArea)
 229  2
                 writer.attribute("value", value);
 230  
             else
 231  1
                 writer.print(value);
 232  
         }
 233  
 
 234  3
         if (!isTextArea)
 235  2
             writer.closeTag();
 236  
         else
 237  1
             writer.end();
 238  
 
 239  3
         renderDelegateSuffix(writer, cycle);
 240  
 
 241  
         // render update element
 242  
 
 243  3
         writer.begin("div");
 244  3
         writer.attribute("id", getClientId() + "choices");
 245  3
         writer.attribute("class", getUpdateElementClass());
 246  3
         writer.end();
 247  
 
 248  
         // render javascript
 249  
 
 250  3
         JSONObject json = null;
 251  3
         String options = getOptions();
 252  
 
 253  
         try {
 254  
 
 255  3
             json = options != null ? new JSONObject(options) : new JSONObject();
 256  
 
 257  1
         } catch (ParseException ex)
 258  
         {
 259  1
             throw new ApplicationRuntimeException(ScriptaculousMessages.invalidOptions(options, ex), this.getBinding("options").getLocation(), ex);
 260  2
         }
 261  
 
 262  
         // bind onFailure client side function if not already defined
 263  
 
 264  2
         if (!json.has("onFailure"))
 265  
         {
 266  2
             json.put("onFailure", "tapestry.error");
 267  
         }
 268  
 
 269  2
         if (!json.has("encoding"))
 270  
         {
 271  2
             json.put("encoding", cycle.getEngine().getOutputEncoding());
 272  
         }
 273  
 
 274  2
         Map parms = new HashMap();
 275  2
         parms.put("inputId", getClientId());
 276  2
         parms.put("updateId", getClientId() + "choices");
 277  2
         parms.put("options", json.toString());
 278  
 
 279  2
         Object[] specifiedParams = DirectLink.constructServiceParameters(getParameters());
 280  2
         Object[] listenerParams = null;
 281  2
         if (specifiedParams != null)
 282  
         {
 283  0
             listenerParams = new Object[specifiedParams.length + 1];
 284  0
             System.arraycopy(specifiedParams, 0, listenerParams, 1, specifiedParams.length);
 285  
         } else {
 286  
 
 287  2
             listenerParams = new Object[1];
 288  
         }
 289  
 
 290  2
         listenerParams[0] = getClientId();
 291  
 
 292  2
         ILink updateLink = getEngineService().getLink(isStateful(), new DirectServiceParameter(this, listenerParams));
 293  2
         parms.put("updateUrl", updateLink.getURL());
 294  
 
 295  2
         PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this);
 296  2
         getScript().execute(this, cycle, pageRenderSupport, parms);
 297  2
     }
 298  
 
 299  
     /**
 300  
      * Rewinds the component, doing translation, validation and binding.
 301  
      */
 302  
     protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
 303  
     {
 304  0
         String value = cycle.getParameter(getName());
 305  
         try
 306  
         {
 307  0
             Object object = getTranslatedFieldSupport().parse(this, value);
 308  0
             getValidatableFieldSupport().validate(this, writer, cycle, object);
 309  
 
 310  0
             setValue(object);
 311  0
         } catch (ValidatorException e)
 312  
         {
 313  0
             getForm().getDelegate().recordFieldInputValue(value);
 314  0
             getForm().getDelegate().record(e);
 315  0
         }
 316  0
     }
 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  0
         IActionListener listener = getListener();
 325  0
         if (listener == null)
 326  0
             throw Tapestry.createRequiredParameterException(this, "listener");
 327  
 
 328  0
         Object[] params = cycle.getListenerParameters();
 329  
 
 330  
         // replace the first param with the correct value
 331  0
         String inputId = (String)params[0];
 332  0
         params[0] = cycle.getParameter(inputId);
 333  
 
 334  0
         cycle.setListenerParameters(params);
 335  
 
 336  0
         setSearchTriggered(true);
 337  
 
 338  0
         getListenerInvoker().invokeListener(listener, this, cycle);
 339  0
     }
 340  
 
 341  
     public List getUpdateComponents()
 342  
     {
 343  2
         return Arrays.asList(new Object[] { getClientId() });
 344  
     }
 345  
 
 346  
     public boolean isAsync()
 347  
     {
 348  2
         return true;
 349  
     }
 350  
 
 351  
     public boolean isJson()
 352  
     {
 353  2
         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  0
         setListItemRenderer(DefaultListItemRenderer.SHARED_INSTANCE);
 363  0
     }
 364  
 }