Coverage Report - org.apache.tapestry.scriptaculous.Suggest
 
Classes in this File Line Coverage Branch Coverage Complexity
Suggest
72% 
94% 
1.667
 
 1  
 package org.apache.tapestry.scriptaculous;
 2  
 
 3  
 import java.text.ParseException;
 4  
 import java.util.Arrays;
 5  
 import java.util.HashMap;
 6  
 import java.util.Iterator;
 7  
 import java.util.List;
 8  
 import java.util.Map;
 9  
 
 10  
 import org.apache.hivemind.ApplicationRuntimeException;
 11  
 import org.apache.hivemind.util.Defense;
 12  
 import org.apache.tapestry.IActionListener;
 13  
 import org.apache.tapestry.IDirect;
 14  
 import org.apache.tapestry.IForm;
 15  
 import org.apache.tapestry.IMarkupWriter;
 16  
 import org.apache.tapestry.IRequestCycle;
 17  
 import org.apache.tapestry.IScript;
 18  
 import org.apache.tapestry.PageRenderSupport;
 19  
 import org.apache.tapestry.TapestryUtils;
 20  
 import org.apache.tapestry.coerce.ValueConverter;
 21  
 import org.apache.tapestry.engine.DirectServiceParameter;
 22  
 import org.apache.tapestry.engine.IEngineService;
 23  
 import org.apache.tapestry.engine.ILink;
 24  
 import org.apache.tapestry.form.AbstractFormComponent;
 25  
 import org.apache.tapestry.form.TranslatedField;
 26  
 import org.apache.tapestry.form.TranslatedFieldSupport;
 27  
 import org.apache.tapestry.form.ValidatableFieldSupport;
 28  
 import org.apache.tapestry.json.JSONObject;
 29  
 import org.apache.tapestry.link.DirectLink;
 30  
 import org.apache.tapestry.listener.ListenerInvoker;
 31  
 import org.apache.tapestry.services.ResponseBuilder;
 32  
 import org.apache.tapestry.util.SizeRestrictingIterator;
 33  
 import org.apache.tapestry.valid.ValidatorException;
 34  
 
 35  
 /**
 36  
  * Implementation of the <a href="http://wiki.script.aculo.us/scriptaculous/show/Ajax.Autocompleter">Ajax.Autocompleter</a> in
 37  
  * the form of a {@link org.apache.tapestry.form.TextField} like component with the additional ability to dynamically suggest
 38  
  * values via XHR requests.
 39  
  *
 40  
  * <p>
 41  
  * This component will use the html element tag name defined in your html template to include it to determine whether or not
 42  
  * to render a TextArea or TextField style input element. For example, specifying a component definition such as:
 43  
  * </p>
 44  
  *
 45  
  * <pre>&lt;input jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
 46  
  *
 47  
  * <p>
 48  
  * would render something looking like:
 49  
  * </p>
 50  
  *
 51  
  * <pre>&lt;input type="text" name="suggest" id="suggest" autocomplete="off" value="literal:A default value" /&gt;</pre>
 52  
  *
 53  
  * <p>while a defintion of</p>
 54  
  *
 55  
  * <pre>&lt;textarea jwcid="@Suggest" value="literal:A default value" /&gt;</pre>
 56  
  *
 57  
  * <p>would render something like:</p>
 58  
  *
 59  
  * <pre>
 60  
  *  &lt;textarea name="suggest" id="suggest" &gt;A default value&lt;textarea/&gt;
 61  
  * </pre>
 62  
  *
 63  
  */
 64  4
 public abstract class Suggest extends AbstractFormComponent implements TranslatedField, IDirect {
 65  
 
 66  
     /**
 67  
      * Injected service used to invoke whatever listeners people have setup to handle
 68  
      * changing value from this field.
 69  
      *
 70  
      * @return The invoker.
 71  
      */
 72  
     public abstract ListenerInvoker getListenerInvoker();
 73  
 
 74  
     /**
 75  
      * Injected response builder for doing specific XHR things.
 76  
      *
 77  
      * @return ResponseBuilder for this request. 
 78  
      */
 79  
     public abstract ResponseBuilder getResponse();
 80  
 
 81  
     /**
 82  
      * Associated javascript template.
 83  
      *
 84  
      * @return The script template.
 85  
      */
 86  
     public abstract IScript getScript();
 87  
 
 88  
     /**
 89  
      * Used to convert form input values.
 90  
      *
 91  
      * @return The value converter to use.
 92  
      */
 93  
     public abstract ValueConverter getValueConverter();
 94  
 
 95  
     /**
 96  
      * Injected.
 97  
      *
 98  
      * @return Service used to validate input.
 99  
      */
 100  
     public abstract ValidatableFieldSupport getValidatableFieldSupport();
 101  
 
 102  
     /**
 103  
      * Injected.
 104  
      *
 105  
      * @return Translation service.
 106  
      */
 107  
     public abstract TranslatedFieldSupport getTranslatedFieldSupport();
 108  
 
 109  
     /**
 110  
      * Injected.
 111  
      *
 112  
      * @return The {@link org.apache.tapestry.engine.DirectService} engine.  
 113  
      */
 114  
     public abstract IEngineService getEngineService();
 115  
 
 116  
     ////////////////////////////////////////////////////////
 117  
     // Parameters
 118  
     ////////////////////////////////////////////////////////
 119  
 
 120  
     public abstract Object getValue();
 121  
     public abstract void setValue(Object value);
 122  
 
 123  
     public abstract ListItemRenderer getListItemRenderer();
 124  
     public abstract void setListItemRenderer(ListItemRenderer renderer);
 125  
 
 126  
     public abstract IActionListener getListener();
 127  
 
 128  
     public abstract Object getListSource();
 129  
     public abstract void setListSource(Object value);
 130  
 
 131  
     public abstract int getMaxResults();
 132  
 
 133  
     public abstract Object getParameters();
 134  
 
 135  
     public abstract String getOptions();
 136  
 
 137  
     public abstract String getUpdateElementClass();
 138  
 
 139  
     /**
 140  
      * Used internally to track listener invoked searches versus
 141  
      * normal rendering requests.
 142  
      *
 143  
      * @return True if search was triggered, false otherwise.
 144  
      */
 145  
     public abstract boolean isSearchTriggered();
 146  
     public abstract void setSearchTriggered(boolean value);
 147  
 
 148  
     public boolean isRequired()
 149  
     {
 150  0
         return getValidatableFieldSupport().isRequired(this);
 151  
     }
 152  
 
 153  
     protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
 154  
     {
 155  
         // render search triggered response instead of normal render if
 156  
         // listener was invoked
 157  
 
 158  4
         IForm form = TapestryUtils.getForm(cycle, this);
 159  4
         setForm(form);
 160  
 
 161  4
         if (form.wasPrerendered(writer, this))
 162  0
             return;
 163  
 
 164  4
         if (!form.isRewinding() && !cycle.isRewinding()
 165  
             && getResponse().isDynamic() && isSearchTriggered())
 166  
         {
 167  1
             setName(form);
 168  
 
 169  
             // do nothing if it wasn't for this instance - such as in a loop
 170  
 
 171  1
             if (cycle.getParameter(getClientId()) == null)
 172  0
                 return;
 173  
 
 174  1
             renderList(writer, cycle);
 175  1
             return;
 176  
         }
 177  
 
 178  
         // defer to super if normal render
 179  
 
 180  3
         super.renderComponent(writer, cycle);
 181  2
     }
 182  
 
 183  
     /**
 184  
      * Invoked only when a search has been triggered to render out the &lt;li&gt; list of
 185  
      * dynamic suggestion options.
 186  
      *
 187  
      * @param writer
 188  
      *          The markup writer.
 189  
      * @param cycle
 190  
      *          The associated request.
 191  
      */
 192  
     public void renderList(IMarkupWriter writer, IRequestCycle cycle)
 193  
     {
 194  1
         Defense.notNull(getListSource(), "listSource for Suggest component.");
 195  
 
 196  2
         Iterator values = (Iterator)getValueConverter().coerceValue(getListSource(), Iterator.class);
 197  
 
 198  1
         if (isParameterBound("maxResults"))
 199  
         {
 200  0
             values = new SizeRestrictingIterator(values, getMaxResults());
 201  
         }
 202  
 
 203  1
         getListItemRenderer().renderList(writer, cycle, values);
 204  1
     }
 205  
 
 206  
     protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
 207  
     {
 208  3
         String value = getTranslatedFieldSupport().format(this, getValue());
 209  3
         boolean isTextArea = getTemplateTagName().equalsIgnoreCase("textarea");
 210  
 
 211  3
         renderDelegatePrefix(writer, cycle);
 212  
 
 213  3
         if (isTextArea)
 214  1
             writer.begin(getTemplateTagName());
 215  
         else
 216  2
             writer.beginEmpty(getTemplateTagName());
 217  
 
 218  
         // only render input attributes if not a textarea
 219  3
         if (!isTextArea)
 220  
         {
 221  2
             writer.attribute("type", "text");
 222  2
             writer.attribute("autocomplete", "off");
 223  
         }
 224  
 
 225  3
         renderIdAttribute(writer, cycle);
 226  3
         writer.attribute("name", getName());
 227  
 
 228  3
         if (isDisabled())
 229  0
             writer.attribute("disabled", "disabled");
 230  
 
 231  3
         renderInformalParameters(writer, cycle);
 232  3
         renderDelegateAttributes(writer, cycle);
 233  
 
 234  3
         getTranslatedFieldSupport().renderContributions(this, writer, cycle);
 235  3
         getValidatableFieldSupport().renderContributions(this, writer, cycle);
 236  
 
 237  3
         if (value != null)
 238  
         {
 239  3
             if (!isTextArea)
 240  2
                 writer.attribute("value", value);
 241  
             else
 242  1
                 writer.print(value);
 243  
         }
 244  
 
 245  3
         if (!isTextArea)
 246  2
             writer.closeTag();
 247  
         else
 248  1
             writer.end();
 249  
 
 250  3
         renderDelegateSuffix(writer, cycle);
 251  
 
 252  
         // render update element
 253  
 
 254  3
         writer.begin("div");
 255  3
         writer.attribute("id", getClientId() + "choices");
 256  3
         writer.attribute("class", getUpdateElementClass());
 257  3
         writer.end();
 258  
 
 259  
         // render javascript
 260  
 
 261  3
         JSONObject json = null;
 262  3
         String options = getOptions();
 263  
 
 264  
         try {
 265  
 
 266  3
             json = options != null ? new JSONObject(options) : new JSONObject();
 267  
 
 268  1
         } catch (ParseException ex)
 269  
         {
 270  1
             throw new ApplicationRuntimeException(ScriptaculousMessages.invalidOptions(options, ex), this.getBinding("options").getLocation(), ex);
 271  2
         }
 272  
 
 273  
         // bind onFailure client side function if not already defined
 274  
 
 275  2
         if (!json.has("onFailure"))
 276  
         {
 277  2
             json.put("onFailure", "tapestry.error");
 278  
         }
 279  
 
 280  2
         if (!json.has("encoding"))
 281  
         {
 282  2
             json.put("encoding", cycle.getEngine().getOutputEncoding());
 283  
         }
 284  
 
 285  2
         Map parms = new HashMap();
 286  2
         parms.put("inputId", getClientId());
 287  2
         parms.put("updateId", getClientId() + "choices");
 288  2
         parms.put("options", json.toString());
 289  
 
 290  2
         Object[] specifiedParams = DirectLink.constructServiceParameters(getParameters());
 291  2
         Object[] listenerParams = null;
 292  2
         if (specifiedParams != null)
 293  
         {
 294  0
             listenerParams = new Object[specifiedParams.length + 1];
 295  0
             System.arraycopy(specifiedParams, 0, listenerParams, 1, specifiedParams.length);
 296  
         } else {
 297  
 
 298  2
             listenerParams = new Object[1];
 299  
         }
 300  
 
 301  2
         listenerParams[0] = getClientId();
 302  
 
 303  2
         ILink updateLink = getEngineService().getLink(isStateful(), new DirectServiceParameter(this, listenerParams));
 304  2
         parms.put("updateUrl", updateLink.getURL());
 305  
 
 306  2
         PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(cycle, this);
 307  2
         getScript().execute(this, cycle, pageRenderSupport, parms);
 308  2
     }
 309  
 
 310  
     /**
 311  
      * Rewinds the component, doing translation, validation and binding.
 312  
      */
 313  
     protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
 314  
     {
 315  0
         String value = cycle.getParameter(getName());
 316  
         try
 317  
         {
 318  0
             Object object = getTranslatedFieldSupport().parse(this, value);
 319  0
             getValidatableFieldSupport().validate(this, writer, cycle, object);
 320  
 
 321  0
             setValue(object);
 322  0
         } catch (ValidatorException e)
 323  
         {
 324  0
             getForm().getDelegate().recordFieldInputValue(value);
 325  0
             getForm().getDelegate().record(e);
 326  0
         }
 327  0
     }
 328  
 
 329  
     /**
 330  
      * Triggers the listener. The parameters passed are the current text
 331  
      * and those specified in the parameters parameter of the component.
 332  
      * If the listener parameter is not bound, attempt to locate an implicit
 333  
      * listener named by the capitalized component id, prefixed by "do".
 334  
      */
 335  
     public void trigger(IRequestCycle cycle)
 336  
     {
 337  0
         IActionListener listener = getListener();
 338  0
         if (listener == null)
 339  0
             listener = getContainer().getListeners().getImplicitListener(this);
 340  
 
 341  0
         Object[] params = cycle.getListenerParameters();
 342  
 
 343  
         // replace the first param with the correct value
 344  0
         String inputId = (String)params[0];
 345  0
         params[0] = cycle.getParameter(inputId);
 346  
 
 347  0
         cycle.setListenerParameters(params);
 348  
 
 349  0
         setSearchTriggered(true);
 350  
 
 351  0
         getListenerInvoker().invokeListener(listener, this, cycle);
 352  0
     }
 353  
 
 354  
     public List getUpdateComponents()
 355  
     {
 356  2
         return Arrays.asList(new Object[] { getClientId() });
 357  
     }
 358  
 
 359  
     public boolean isAsync()
 360  
     {
 361  2
         return true;
 362  
     }
 363  
 
 364  
     public boolean isJson()
 365  
     {
 366  2
         return false;
 367  
     }
 368  
 
 369  
     /**
 370  
      * Sets the default {@link ListItemRenderer} for component, to be overriden as
 371  
      * necessary by component parameters.
 372  
      */
 373  
     protected void finishLoad()
 374  
     {
 375  0
         setListItemRenderer(DefaultListItemRenderer.SHARED_INSTANCE);
 376  0
     }
 377  
 }