001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.html.link;
018
019import org.apache.wicket.Component;
020import org.apache.wicket.IGenericComponent;
021import org.apache.wicket.IRequestListener;
022import org.apache.wicket.Page;
023import org.apache.wicket.WicketRuntimeException;
024import org.apache.wicket.markup.ComponentTag;
025import org.apache.wicket.markup.head.IHeaderResponse;
026import org.apache.wicket.markup.head.OnEventHeaderItem;
027import org.apache.wicket.model.IModel;
028import org.apache.wicket.request.mapper.parameter.PageParameters;
029
030/**
031 * Implementation of a hyperlink component. A link can be used with an anchor (<a href...)
032 * element or any element that supports the onclick javascript event handler (such as buttons, td
033 * elements, etc). When used with an anchor, a href attribute will be generated. When used with any
034 * other element, a click javascript event handler will be added.
035 * <p>
036 * You can use a link like:
037 * 
038 * <pre>
039 * add(new Link(&quot;myLink&quot;)
040 * {
041 *     public void onClick()
042 *     {
043 *         // do something here...
044 *     }
045 * );
046 * </pre>
047 * 
048 * and in your HTML file:
049 * 
050 * <pre>
051 *  &lt;a href=&quot;#&quot; wicket:id=&quot;myLink&quot;&gt;click here&lt;/a&gt;
052 * </pre>
053 * 
054 * or:
055 * 
056 * <pre>
057 *  &lt;td wicket:id=&quot;myLink&quot;&gt;my clickable column&lt;/td&gt;
058 * </pre>
059 * 
060 * <p>
061 * The following snippet shows how to pass a parameter from the Page creating the Page to the Page
062 * responded by the Link.
063 * 
064 * <pre>
065 * add(new Link&lt;MyObject&gt;(&quot;link&quot;, listItem.getModel())
066 * {
067 *     public void onClick()
068 *     {
069 *         MyObject obj = getModelObject();
070 *         setResponsePage(new MyPage(obj));
071 *     }
072 * </pre>
073 * 
074 * @author Jonathan Locke
075 * @author Eelco Hillenius
076 * @param <T>
077 *            type of model object
078 */
079public abstract class Link<T> extends AbstractLink implements IRequestListener, IGenericComponent<T, Link<T>>
080{
081        private static final long serialVersionUID = 1L;
082
083        /**
084         * An anchor (form 'http://server/app/etc#someAnchor') will be appended to the link so that
085         * after this link executes, it will jump to the provided anchor component's position. The
086         * provided anchor must either have the {@link Component#getOutputMarkupId()} flag true, or it
087         * must be attached to a &lt;a tag with a href attribute of more than one character starting
088         * with '#' ('&lt;a href="#someAnchor" ... ').
089         */
090        private Component anchor;
091
092        /**
093         * True if link should automatically enable/disable based on current page; false by default.
094         */
095        private boolean autoEnable = false;
096
097        /**
098         * The popup specification. If not-null, a javascript on-click event handler will be generated
099         * that opens a new window using the popup properties.
100         */
101        private PopupSettings popupSettings = null;
102
103        /**
104         * @see org.apache.wicket.Component#Component(String)
105         */
106        public Link(final String id)
107        {
108                super(id);
109        }
110
111        /**
112         * @param id
113         * @param model
114         * @see org.apache.wicket.Component#Component(String, IModel)
115         */
116        public Link(final String id, IModel<T> model)
117        {
118                super(id, model);
119        }
120
121        /**
122         * Gets any anchor component.
123         * 
124         * @return Any anchor component to jump to, might be null
125         */
126        public Component getAnchor()
127        {
128                return anchor;
129        }
130
131        /**
132         * Gets whether link should automatically enable/disable based on current page.
133         * 
134         * @return Whether this link should automatically enable/disable based on current page.
135         */
136        public final boolean getAutoEnable()
137        {
138                return autoEnable;
139        }
140
141        /**
142         * Gets the popup specification. If not-null, a javascript on-click event handler will be
143         * generated that opens a new window using the popup properties.
144         * 
145         * @return the popup specification.
146         */
147        public final PopupSettings getPopupSettings()
148        {
149                return popupSettings;
150        }
151
152        /**
153         * @see org.apache.wicket.Component#isEnabled()
154         */
155        @Override
156        public boolean isEnabled()
157        {
158                // If we're auto-enabling
159                if (getAutoEnable())
160                {
161                        // the link is enabled if this link doesn't link to the current page
162                        return !linksTo(getPage());
163                }
164                return super.isEnabled();
165        }
166
167        /**
168         * @see org.apache.wicket.Component#getStatelessHint()
169         */
170        @Override
171        protected boolean getStatelessHint()
172        {
173                return false;
174        }
175
176        /**
177         * Called when a link is clicked.
178         */
179        public abstract void onClick();
180
181        /**
182         * THIS METHOD IS NOT PART OF THE WICKET API. DO NOT ATTEMPT TO OVERRIDE OR CALL IT.
183         * 
184         * Called when a link is clicked. The implementation of this method is currently to simply call
185         * onClick(), but this may be augmented in the future.
186         */
187        @Override
188        public void onRequest()
189        {
190                // Invoke subclass handler
191                onClick();
192        }
193
194        /**
195         * Sets an anchor component. An anchor (form 'http://server/app/etc#someAnchor') will be
196         * appended to the link so that after this link executes, it will jump to the provided anchor
197         * component's position. The provided anchor must either have the
198         * {@link Component#getOutputMarkupId()} flag true, or it must be attached to a &lt;a tag with a
199         * href attribute of more than one character starting with '#' ('&lt;a href="#someAnchor" ...
200         * ').
201         * 
202         * @param anchor
203         *            The anchor
204         * @return this
205         */
206        public Link<T> setAnchor(Component anchor)
207        {
208                addStateChange();
209                this.anchor = anchor;
210                return this;
211        }
212
213        /**
214         * Sets whether this link should automatically enable/disable based on current page.
215         * 
216         * @param autoEnable
217         *            whether this link should automatically enable/disable based on current page.
218         * @return This
219         */
220        public final Link<T> setAutoEnable(final boolean autoEnable)
221        {
222                this.autoEnable = autoEnable;
223                return this;
224        }
225
226        /**
227         * Sets the popup specification. If not-null, a javascript on-click event handler will be
228         * generated that opens a new window using the popup properties.
229         * 
230         * @param popupSettings
231         *            the popup specification.
232         * @return This
233         */
234        public final Link<T> setPopupSettings(final PopupSettings popupSettings)
235        {
236                this.popupSettings = popupSettings;
237                return this;
238        }
239
240        /**
241         * Appends any anchor to the url if the url is not null and the url does not already contain an
242         * anchor (url.indexOf('#') != -1). This implementation looks whether an anchor component was
243         * set, and if so, it will append the markup id of that component. That markup id is gotten by
244         * either calling {@link Component#getMarkupId()} if {@link Component#getOutputMarkupId()}
245         * returns true, or if the anchor component does not output it's id, this method will try to
246         * retrieve the id from the markup directly. If neither is found, an
247         * {@link WicketRuntimeException exception} is thrown. If no anchor component was set, but the
248         * link component is attached to a &lt;a element, this method will append what is in the href
249         * attribute <i>if</i> there is one, starts with a '#' and has more than one character.
250         * <p>
251         * You can override this method, but it means that you have to take care of whatever is done
252         * with any set anchor component yourself. You also have to manually append the '#' at the right
253         * place.
254         * </p>
255         * 
256         * @param tag
257         *            The component tag
258         * @param url
259         *            The url to start with
260         * @return The url, possibly with an anchor appended
261         */
262        protected CharSequence appendAnchor(final ComponentTag tag, CharSequence url)
263        {
264                if (url != null)
265                {
266                        Component anchor = getAnchor();
267                        if (anchor != null)
268                        {
269                                if (url.toString().indexOf('#') == -1)
270                                {
271                                        String id;
272                                        if (anchor.getOutputMarkupId())
273                                        {
274                                                id = anchor.getMarkupId();
275                                        }
276                                        else
277                                        {
278                                                id = anchor.getMarkupAttributes().getString("id");
279                                        }
280
281                                        if (id != null)
282                                        {
283                                                url = url + "#" + anchor.getMarkupId();
284                                        }
285                                        else
286                                        {
287                                                throw new WicketRuntimeException("an achor component was set on " + this +
288                                                        " but it neither has outputMarkupId set to true " +
289                                                        "nor has a id set explicitly");
290                                        }
291                                }
292                        }
293                        else
294                        {
295                                if (tag.getName().equalsIgnoreCase("a"))
296                                {
297                                        if (url.toString().indexOf('#') == -1)
298                                        {
299                                                String href = tag.getAttributes().getString("href");
300                                                if (href != null && href.length() > 1 && href.charAt(0) == '#')
301                                                {
302                                                        url = url + href;
303                                                }
304                                        }
305                                }
306                        }
307                }
308                return url;
309        }
310
311        /**
312         * @param url
313         *            The url for the link
314         * @return Any onClick JavaScript that should be used
315         */
316        protected CharSequence getOnClickScript(final CharSequence url)
317        {
318                return null;
319        }
320
321        /**
322         * Gets the url to use for this link.
323         * 
324         * @return The URL that this link links to
325         */
326        protected CharSequence getURL()
327        {
328                return urlForListener(new PageParameters());
329        }
330
331        /**
332         * Whether this link refers to the given page.
333         * 
334         * @param page
335         *            A page
336         * @return True if this link goes to the given page
337         */
338        protected boolean linksTo(final Page page)
339        {
340                return false;
341        }
342
343        /**
344         * Handles this link's tag. OVERRIDES MUST CALL SUPER.
345         * 
346         * @param tag
347         *            the component tag
348         * @see org.apache.wicket.Component#onComponentTag(ComponentTag)
349         */
350        @Override
351        protected void onComponentTag(final ComponentTag tag)
352        {
353                // Default handling for tag
354                super.onComponentTag(tag);
355
356                // If we're disabled
357                if (isEnabledInHierarchy())
358                {
359                        // Set href to link to this link's linkClicked method
360                        CharSequence url = getURL();
361
362                        // append any anchor
363                        url = appendAnchor(tag, url);
364
365                        // if the tag is an anchor proper
366                        if (tag.getName().equalsIgnoreCase("a") || tag.getName().equalsIgnoreCase("link") ||
367                                tag.getName().equalsIgnoreCase("area"))
368                        {
369                                // generate the href attribute
370                                tag.put("href", url);
371                        }
372                        else if (tag.getName().equalsIgnoreCase("script") ||
373                                tag.getName().equalsIgnoreCase("style"))
374                        {
375                                tag.put("src", url);
376                        }
377                }
378                else
379                {
380                        disableLink(tag);
381                }
382        }
383        
384        @Override
385        public void renderHead(IHeaderResponse response)
386        {
387                super.renderHead(response);
388                // If we're disabled
389                if (isEnabledInHierarchy() && useJSEventBindingWhenNeeded())
390                {
391                        ComponentTag tag = getMarkupTag();
392
393                        // Set href to link to this link's linkClicked method
394                        CharSequence url = getURL();
395
396                        // append any anchor
397                        url = appendAnchor(tag, url);
398
399                        // If the subclass specified javascript, use that
400                        final CharSequence onClickJavaScript = getOnClickScript(url);
401                        if (onClickJavaScript != null)
402                        {
403                                response.render(OnEventHeaderItem.forComponent(this, "click", onClickJavaScript));
404                                return;
405                        }
406
407                        // next check for popup settings
408                        if (popupSettings != null)
409                        {
410                                popupSettings.setTarget("'" + url + "'");
411                                response.render(OnEventHeaderItem.forComponent(this, "click",
412                                        popupSettings.getPopupJavaScript()));
413                                return;
414                        }
415
416                        // finally, when the tag is not a normal link
417                        if (!(tag.getName().equalsIgnoreCase("a") || tag.getName().equalsIgnoreCase("link")
418                                || tag.getName().equalsIgnoreCase("area")
419                                || tag.getName().equalsIgnoreCase("script")
420                                || tag.getName().equalsIgnoreCase("style")))
421                        {
422                                // generate an onclick JS handler directly
423                                // in firefox when the element is quickly clicked 3 times a second request is
424                                // generated during page load. This check ensures that the click is ignored
425                                response.render(OnEventHeaderItem.forComponent(this, "click",
426                                        "var win = this.ownerDocument.defaultView || this.ownerDocument.parentWindow; "
427                                                + "if (win == window) { window.location.href='" + url
428                                                + "'; } ;return false"));
429                                return;
430                        }
431                }
432        }
433        
434        /**
435         * This method can be overridden by a subclass to disable the JS event binding or provide custom
436         * event binding code is used.
437         * 
438         * @return true when a javascripot event binding must used to handle the click event.
439         */
440        protected boolean useJSEventBindingWhenNeeded()
441        {
442                return true;
443        }
444}