You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@click.apache.org by sa...@apache.org on 2009/03/09 19:20:16 UTC

svn commit: r751785 [1/2] - in /incubator/click/trunk/click/framework/src/org/apache/click: ./ control/ util/

Author: sabob
Date: Mon Mar  9 18:20:16 2009
New Revision: 751785

URL: http://svn.apache.org/viewvc?rev=751785&view=rev
Log:
Added first class support for HTML Headers. CLK-501

Added:
    incubator/click/trunk/click/framework/src/org/apache/click/util/Css.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/CssImport.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/HtmlHeader.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/Javascript.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/JavascriptImport.java
Modified:
    incubator/click/trunk/click/framework/src/org/apache/click/Control.java
    incubator/click/trunk/click/framework/src/org/apache/click/Page.java
    incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractContainer.java
    incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractControl.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/ClickUtils.java
    incubator/click/trunk/click/framework/src/org/apache/click/util/PageImports.java

Modified: incubator/click/trunk/click/framework/src/org/apache/click/Control.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/Control.java?rev=751785&r1=751784&r2=751785&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/Control.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/Control.java Mon Mar  9 18:20:16 2009
@@ -19,6 +19,7 @@
 package org.apache.click;
 
 import java.io.Serializable;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.ServletContext;
@@ -143,9 +144,9 @@
     public Context getContext();
 
     /**
-     * Return the HTML import string to be include in the page.
+     * Return the HTML import string to be included in the page.
      * <p/>
-     * Override this method to specify JavaScript and CSS includes for the
+     * Implement this method to specify JavaScript and CSS includes for the
      * page. For example:
      *
      * <pre class="codeJava">
@@ -161,19 +162,110 @@
      * parse multiple import lines on the <tt>'\n'</tt> char and ensure that
      * imports are not included twice.
      * <p/>
-     * The order in which JS and CSS files are include will be preserved in the
+     * The order in which JS and CSS files are included will be preserved in the
      * page.
      * <p/>
      * <b>Also note:</b> a common problem when overriding getHtmlImports in
      * subclasses is forgetting to call <em>super.getHtmlImports</em>. Consider
      * carefully whether you should call <em>super.getHtmlImports</em> or not.
      *
+     * @deprecated use the new {@link #getHtmlHeaders()} instead
+     *
      * @return the HTML includes statements for the control stylesheet and
      * JavaScript files, or null if no includes are available
      */
     public String getHtmlImports();
 
     /**
+     * Return the list of {@link org.apache.click.util.HtmlHeader HTML HEAD entries}
+     * to be included in the page. Example HTML headers include
+     * {@link org.apache.click.util.JavascriptImport JavascriptImport},
+     * {@link org.apache.click.util.Javascript inline Javascript},
+     * {@link org.apache.click.util.CssImport CssImport} and
+     * {@link org.apache.click.util.Css inline CSS}.
+     * <p/>
+     * Controls can include their own list of HTML HEAD entries by implementing
+     * this method.
+     * <p/>
+     * The recommended approach when implementing this method is to use
+     * <tt>lazy loading</tt> to only add HTML headers <tt>once</tt> and when
+     * <tt>needed</tt>.
+     * For example:
+     *
+     * <pre class="prettyprint">
+     * public MyControl extends AbstractControl {
+     *
+     *     public List getHtmlHeaders() {
+     *         // Use lazy loading to ensure the JS is only added the
+     *         // first time this method is called.
+     *         if (htmlHeaders == null) {
+     *             // Get the header entries from the super implementation
+     *             htmlHeaders = super.getHtmlHeaders();
+     *
+     *             // Include the control's external Javascript resource
+     *             JavascriptImport jsImport = new JavascriptImport("/mycorp/mycontrol/mycontrol.js");
+     *             htmlHeaders.add(jsImport);
+     *
+     *             // Include the control's external Css resource
+     *             CssImport cssImport = new CssImport("/mycorp/mycontrol/mycontrol.css");
+     *             htmlHeaders.add(cssImport);
+     *         }
+     *         return htmlHeaders;
+     *     }
+     * } </pre>
+     *
+     * An alternative is to add the HTML headers in the Control's constructor:
+     *
+     * <pre class="prettyprint">
+     * public MyControl extends AbstractControl {
+     *
+     *     public MyControl() {
+     *         JavascriptImport jsImport = new JavascriptImport("/mycorp/mycontrol/mycontrol.js");
+     *         getHtmlHeaders().add(jsImport);
+     *         CssImport cssImport = new CssImport("/mycorp/mycontrol/mycontrol.css");
+     *         getHtmlHeaders().add(cssImport);
+     *     }
+     * } </pre>
+     *
+     * One can also add HTML headers from event handler methods such as
+     * {@link #onInit()}, {@link #onProcess()}, {@link #onRender()}
+     * etc. However its possible the control will be added to a {@link Page#stateful Stateful}
+     * page, so you will need to set the HTML header list to <tt>null</tt> in the
+     * Control's {@link #onDestroy()} event handler, otherwise the HTML header
+     * list will continue to grow with each request:
+     *
+     * <pre class="prettyprint">
+     * public MyControl extends AbstractControl {
+     *
+     *     public void onInit() {
+     *         // Add HTML headers
+     *         JavascriptImport jsImport = new JavascriptImport("/mycorp/mycontrol/mycontrol.js");
+     *         getHtmlHeaders().add(jsImport);
+     *         CssImport cssImport = new CssImport("/mycorp/mycontrol/mycontrol.css");
+     *         getHtmlHeaders().add(cssImport);
+     *     }
+     *
+     *     public void onDestroy() {
+     *         // Nullify the HTML headers
+     *         htmlHeaders = null;
+     *     }
+     * } </pre>
+     *
+     * The order in which JS and CSS files are included will be preserved in the
+     * page.
+     * <p/>
+     * <b>Note:</b> this method must never return null. If no HTML HEAD entries
+     * are available this method must return an empty {@link java.util.List}.
+     * <p/>
+     * <b>Also note:</b> a common problem when overriding getHtmlHeaders in
+     * subclasses is forgetting to call <em>super.getHtmlHeaders</em>. Consider
+     * carefully whether you should call <em>super.getHtmlHeaders</em> or not.
+     *
+     * @return the list of HTML HEAD entries to be included in the page
+     */
+    public List getHtmlHeaders();
+
+    /**
      * Return HTML element identifier attribute "id" value.
      *
      * {@link org.apache.click.control.AbstractControl#getId()}

Modified: incubator/click/trunk/click/framework/src/org/apache/click/Page.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/Page.java?rev=751785&r1=751784&r2=751785&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/Page.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/Page.java Mon Mar  9 18:20:16 2009
@@ -153,6 +153,12 @@
     /** The list of page controls. */
     protected List controls;
 
+    /**
+     * The list of page HTML head entries including: JavaScript imports,
+     * CSS imports, inline JavaScript and inline CSS.
+     */
+    protected List htmlHeaders;
+
     /** The Velocity template formatter object. */
     protected Format format;
 
@@ -636,7 +642,7 @@
      * parse multiple import lines on the <tt>'\n'</tt> char and ensure that
      * imports are not included twice.
      * <p/>
-     * The order in which JS and CSS files are include will be preserved in the
+     * The order in which JS and CSS files are included will be preserved in the
      * page.
      * <p/>
      * If you need to customize the HTML imports included in your page override
@@ -644,12 +650,112 @@
      *
      * @return the HTML includes statements for the control stylesheet and
      * JavaScript files, by default this method returns null
+     *
+     * @deprecated use the new {@link #getHtmlHeaders()} instead
      */
     public String getHtmlImports() {
         return null;
     }
 
     /**
+     * Return the list of {@link org.apache.click.util.HtmlHeader HTML HEAD entries}
+     * to be included in the page. Example HTML headers include
+     * {@link org.apache.click.util.JavascriptImport JavascriptImport},
+     * {@link org.apache.click.util.Javascript inline Javascript},
+     * {@link org.apache.click.util.CssImport CssImport} and
+     * {@link org.apache.click.util.Css inline CSS}.
+     * <p/>
+     * Pages can include their own list of HTML HEAD entries by implementing
+     * this method.
+     * <p/>
+     * The recommended approach when implementing this method is to use
+     * <tt>lazy loading</tt> to only add HTML headers once and when needed.
+     * For example:
+     *
+     * <pre class="prettyprint">
+     * public MyPage extends Page {
+     *
+     *     public List getHtmlHeaders() {
+     *         // Use lazy loading to ensure the JS is only added the
+     *         // first time this method is called.
+     *         if (htmlHeaders == null) {
+     *             // Get the header entries from the super implementation
+     *             htmlHeaders = super.getHtmlHeaders();
+     *
+     *             // Include the page's external Javascript resource
+     *             JavascriptImport jsImport = new JavascriptImport("/mycorp/js/mypage.js");
+     *             htmlHeaders.add(jsImport);
+     *
+     *             // Include the page's external Css resource
+     *             CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
+     *             htmlHeaders.add(cssImport);
+     *         }
+     *         return htmlHeaders;
+     *     }
+     * } </pre>
+     *
+     * An alternative is to add the HTML headers in the Page constructor:
+     *
+     * <pre class="prettyprint">
+     * public MyPage extends Page {
+     *
+     *     public MyPage() {
+     *         JavascriptImport jsImport = new JavascriptImport("/mycorp/js/mypage.js");
+     *         getHtmlHeaders().add(jsImport);
+     *         CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
+     *         getHtmlHeaders().add(cssImport);
+     *     }
+     * } </pre>
+     *
+     * One can also add HTML headers from event handler methods such as
+     * {@link #onInit()}, {@link #onGet()}, {@link #onPost()}, {@link #onRender()}
+     * etc. However when using {@link #stateful Stateful} pages, you will need to
+     * set the HTML header list to <tt>null</tt> in the {@link #onDestroy()} event
+     * handler, otherwise the HTML header list will continue to grow with each
+     * request:
+     *
+     * <pre class="prettyprint">
+     * public MyPage extends Page {
+     *
+     *     public MyPage() {
+     *         // Activate stateful page
+     *         setStateful(true);
+     *     }
+     *
+     *     public void onInit() {
+     *         // Add HTML headers
+     *         JavascriptImport jsImport = new JavascriptImport("/mycorp/js/mypage.js");
+     *         getHtmlHeaders().add(jsImport);
+     *         CssImport cssImport = new CssImport("/mycorp/js/mypage.css");
+     *         getHtmlHeaders().add(cssImport);
+     *     }
+     *
+     *     public void onDestroy() {
+     *         // Nullify the HTML headers
+     *         htmlHeaders = null;
+     *     }
+     * } </pre>
+     *
+     * The order in which JS and CSS files are included will be preserved in the
+     * page.
+     * <p/>
+     * <b>Note:</b> this method must never return null. If no HTML HEAD entries
+     * are available this method must return an empty {@link java.util.List}.
+     * <p/>
+     * <b>Also note:</b> a common problem when overriding getHtmlHeaders in
+     * subclasses is forgetting to call <em>super.getHtmlHeaders</em>. Consider
+     * carefully whether you should call <em>super.getHtmlHeaders</em> or not.
+     *
+     * @return the list of HTML HEAD entries to be included in the page
+     */
+    public List getHtmlHeaders() {
+        if (htmlHeaders == null) {
+            htmlHeaders = new ArrayList(2);
+        }
+        return htmlHeaders;
+    }
+
+    /**
      * Return the localized Page resource message for the given resource
      * property key. The resource message returned will use the Locale obtained
      * from the Context.
@@ -944,13 +1050,14 @@
      * Set the location to redirect the request to.
      * <p/>
      * If the {@link #redirect} property is not null it will be used to redirect
-     * the request in preference to {@link #forward} or {@link #path} properties.
-     * The request is redirected to using the HttpServletResponse.setRedirect()
+     * the request in preference to the {@link #forward} and {@link #path}
+     * properties. The request is redirected using the HttpServletResponse.setRedirect()
      * method.
      * <p/>
      * If the redirect location begins with a <tt class="wr">"/"</tt>
      * character the redirect location will be prefixed with the web applications
-     * context path.
+     * <tt>context path</tt>. Note if the given location is already prefixed
+     * with the <tt>context path</tt>, Click won't add it a second time.
      * <p/>
      * For example if an application is deployed to the context
      * <tt class="wr">"mycorp"</tt> calling

Modified: incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractContainer.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractContainer.java?rev=751785&r1=751784&r2=751785&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractContainer.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractContainer.java Mon Mar  9 18:20:16 2009
@@ -277,6 +277,8 @@
     /**
      * @see org.apache.click.Control#getHtmlImports()
      *
+     * @deprecated use the new {@link #getHtmlHeaders()} instead
+     *
      * @return the HTML includes statements for the container and child Controls,
      * or null if no includes are available
      */

Modified: incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractControl.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractControl.java?rev=751785&r1=751784&r2=751785&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractControl.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/control/AbstractControl.java Mon Mar  9 18:20:16 2009
@@ -19,10 +19,12 @@
 package org.apache.click.control;
 
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -124,6 +126,12 @@
     /** The control's action listener. */
     protected ActionListener actionListener;
 
+    /**
+     * The list of control HTML head entries including: JavaScript imports,
+     * CSS imports, inline JavaScript and inline CSS.
+     */
+    protected List htmlHeaders;
+
     /** The Control attributes Map. */
     protected Map attributes;
 
@@ -540,6 +548,8 @@
     /**
      * @see org.apache.click.Control#getHtmlImports()
      *
+     * @deprecated use the new {@link #getHtmlHeaders()} instead
+     *
      * @return the HTML includes statements for the control stylesheet and
      * JavaScript files
      */
@@ -548,6 +558,20 @@
     }
 
     /**
+     * @see org.apache.click.Control#getHtmlHeaders()
+     *
+     * @return the list of HTML HEAD entries to be included in the page
+     */
+    public List getHtmlHeaders() {
+        if (htmlHeaders == null) {
+            // Most controls won't provide their own html headers, so save
+            // memory by creating an empty array list
+            htmlHeaders = new ArrayList(0);
+        }
+        return htmlHeaders;
+    }
+
+    /**
      * Return the parent page of this control, or null if not defined.
      *
      * @return the parent page of this control, or null if not defined

Modified: incubator/click/trunk/click/framework/src/org/apache/click/util/ClickUtils.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/ClickUtils.java?rev=751785&r1=751784&r2=751785&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/ClickUtils.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/ClickUtils.java Mon Mar  9 18:20:16 2009
@@ -897,7 +897,7 @@
      * <p/>
      * The version indicator is based on the current Click release version.
      * For example when using Click 1.4 this method will return the string
-     * <tt>"-1.4"</tt>.
+     * <tt>"_1.4"</tt>.
      *
      * @param context the request context
      * @return a version indicator for web resources

Added: incubator/click/trunk/click/framework/src/org/apache/click/util/Css.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/Css.java?rev=751785&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/Css.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/Css.java Mon Mar  9 18:20:16 2009
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.click.util;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * Provides a HEAD entry for importing <tt>inline</tt> Cascading Stylesheets
+ * through the &lt;style&gt; element.
+ * <p/>
+ * Example usage:
+ *
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the CSS import is only added the
+ *         // first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             Css css = new Css("body { font: 12px arial; }");
+ *             htmlHeaders.add(jsImport);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>Css</tt> instance will render the following:
+ *
+ * <pre class="prettyprint">
+ * &lt;style type="text/css" rel="stylesheet"&gt;
+ * body { font: 12px arial; }
+ * &lt;/style&gt;
+ * </pre>
+ *
+ * Below is an example showing how to create inline CSS from a Velocity
+ * template.
+ * <p/>
+ * First we create a Velocity template <tt>(/css/style-template.css)</tt> which
+ * contains the variable <tt>$context</tt> that must be replaced at runtime by
+ * the application <tt>context path</tt>:
+ *
+ * <pre class="prettyprint">
+ * .blue {
+ *     background: #00ff00 url('$context/css/blue.png') no-repeat fixed center;
+ * } </pre>
+ *
+ * Next is the Page implementation:
+ *
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the CSS is only added the first time
+ *         // this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             Context context = getContext();
+ *
+ *             // Create a default template model to pass to the template
+ *             Map model = ClickUtils.createTemplateModel(this, context);
+ *
+ *             // Specify the path to CSS Velocity template
+ *             String cssTemplate = "/css/style-template.css";
+ *
+ *             // Render the template using the model above
+ *             String content = context.renderTemplate(cssTemplate, model);
+ *
+ *             // Create the inline Css for the given template
+ *             Css css = new Css(content);
+ *             htmlHeaders.add(css);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>Css</tt> above will render as follows (assuming the context path is
+ * <tt>myApp</tt>):
+ *
+ * <pre class="prettyprint">
+ * &lt;style type="text/css" rel="stylesheet"&gt;
+ * .blue {
+ *     background: #00ff00 url('/myApp/css/blue.png') no-repeat fixed center;
+ * }
+ * &lt;/style&gt;
+ * </pre>
+ *
+ * @author Bob Schellink
+ */
+public class Css extends HtmlHeader {
+
+    // -------------------------------------------------------------- Variables
+
+    /** A buffer holding the inline CSS content. */
+    private HtmlStringBuffer content = new HtmlStringBuffer();
+
+    // ------------------------------------------------------------ Constructor
+
+    /**
+     * Construct a new inline CSS element.
+     */
+    public Css() {
+        this(null);
+    }
+
+    /**
+     * Construct a new inline CSS element with the given content.
+     *
+     * @param content the CSS content
+     */
+    public Css(String content) {
+        if (content != null) {
+            this.content.append(content);
+        }
+        setAttribute("type", "text/css");
+        setAttribute("rel", "stylesheet");
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Returns the Css HTML tag: &lt;style&gt;.
+     *
+     * @return the Css HTML tag: &lt;style&gt;
+     */
+    public String getTag() {
+        return "style";
+    }
+
+    /**
+     * Return the CSS content buffer.
+     *
+     * @return the CSS content buffer
+     */
+    public HtmlStringBuffer getContent() {
+        return content;
+    }
+
+    /**
+     * Set the CSS content buffer.
+     *
+     * @param content the new content buffer
+     */
+    public void setContent(HtmlStringBuffer content) {
+        this.content = content;
+    }
+
+    /**
+     * Append the given CSS string to the content buffer.
+     *
+     * @param content the CSS string to append to the content buffer
+     */
+    public void append(String content) {
+        this.content.append(content);
+    }
+
+    /**
+     * Render the HTML representation of the CSS to the specified buffer.
+     *
+     * @param buffer the buffer to render output to
+     */
+    public void render(HtmlStringBuffer buffer) {
+
+        // Render IE conditional comment if conditional comment was set
+        renderConditionalCommentPrefix(buffer);
+
+        buffer.elementStart(getTag());
+
+        buffer.appendAttribute("id", getId());
+        appendAttributes(buffer);
+
+        buffer.closeTag();
+
+        // Render CDATA tag if necessary
+        renderCharacterDataPrefix(buffer);
+
+        buffer.append(getContent());
+
+        renderCharacterDataSuffix(buffer);
+
+        buffer.elementEnd(getTag());
+
+        renderConditionalCommentSuffix(buffer);
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     *
+     * @param o the object with which to compare this instance with
+     * @return true if the specified object is the same as this object
+     */
+    public boolean equals(Object o) {
+        if (!isUnique()) {
+            return super.equals(o);
+        }
+
+        //1. Use the == operator to check if the argument is a reference to this object.
+        if (o == this) {
+            return true;
+        }
+
+        //2. Use the instanceof operator to check if the argument is of the correct type.
+        if (!(o instanceof Css)) {
+            return false;
+        }
+
+        //3. Cast the argument to the correct type.
+        Css that = (Css) o;
+
+        String id = getId();
+        String thatId = that.getId();
+        return id == null ? thatId == null : id.equals(thatId);
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     *
+     * @return a hash code value for this object
+     */
+    public int hashCode() {
+        if (!isUnique()) {
+            return super.hashCode();
+        }
+        return new HashCodeBuilder(17, 37).append(getId()).toHashCode();
+    }
+
+    // ------------------------------------------------ Package Private Methods
+
+    /**
+     * @see HtmlHeader#setUnique(boolean)
+     *
+     * @deprecated use {@link #setId(java.lang.String)} instead
+     *
+     * @param unique sets whether the HtmlHeader import should be unique or not
+     */
+    void setUnique(boolean unique) {
+        super.setUnique(unique);
+
+        // If CSS is unique and ID is not defined, derive the ID from the content
+        if (unique && StringUtils.isBlank(getId()) && getContent().length() > 0) {
+            int hash = getContent().toString().hashCode();
+            setId(Integer.toString(hash));
+        }
+    }
+}

Added: incubator/click/trunk/click/framework/src/org/apache/click/util/CssImport.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/CssImport.java?rev=751785&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/CssImport.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/CssImport.java Mon Mar  9 18:20:16 2009
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.click.util;
+
+import org.apache.click.Context;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * Provides a HEAD entry for importing <tt>external</tt> Cascading Stylesheets
+ * through the &lt;link&gt; element.
+ * <p/>
+ * Example usage:
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the CSS import is only added the
+ *         // first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             CssImport cssImport = new CssImport("/css/style.css");
+ *             htmlHeaders.add(cssImport);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>cssImport</tt> above will be rendered as follows (assuming the context
+ * path is <tt>myApp</tt>):
+ * <pre class="prettyprint">
+ * &lt;link type="text/css" rel="stylesheet" href="/myApp/css/style.css"/&gt; </pre>
+ *
+ * @author Bob Schellink
+ */
+public class CssImport extends HtmlHeader {
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * Constructs a new CssImport link.
+     */
+    public CssImport() {
+        this(null);
+    }
+
+    /**
+     * Construct a new CssImport link with the specified <tt>href</tt> attribute.
+     * <p/>
+     * <b>Please note</b> if the given <tt>href</tt> begins with a <tt class="wr">"/"</tt>
+     * character the href will be prefixed with the web application
+     * <tt>context path</tt>.
+     *
+     * @param href the CSS link href attribute
+     */
+    public CssImport(String href) {
+        setHref(href);
+        setAttribute("type", "text/css");
+        setAttribute("rel", "stylesheet");
+    }
+
+    // ------------------------------------------------------ Public Properties
+
+    /**
+     * Returns the Css import HTML tag: &lt;link&gt;.
+     *
+     * @return the Css import HTML tag: &lt;link&gt;
+     */
+    public String getTag() {
+        return "link";
+    }
+
+    /**
+     * This method always return true because CSS import must be unique based on
+     * its <tt>href</tt> attribute. In other words the Page HEAD should only
+     * contain a single CSS import for the specific <tt>href</tt>.
+     *
+     * @see HtmlHeader#isUnique()
+     *
+     * @return true because CSS import must unique based on its <tt>href</tt>
+     * attribute
+     */
+    public boolean isUnique() {
+        return true;
+    }
+
+    /**
+     * Sets the <tt>href</tt> attribute.
+     * <p/>
+     * If the given <tt>href</tt> begins with a <tt class="wr">"/"</tt> character
+     * the href will be prefixed with the web applications <tt>context path</tt>.
+     * Note if the given href is already prefixed with the <tt>context path</tt>,
+     * Click won't add it a second time.
+     *
+     * @param href the new href attribute
+     */
+    public void setHref(String href) {
+        if (href != null) {
+            if (href.charAt(0) == '/') {
+                Context context = getContext();
+                String contextPath = context.getRequest().getContextPath();
+
+                // Guard against adding duplicate context path
+                if (!href.startsWith(contextPath + '/')) {
+                    HtmlStringBuffer buffer =
+                        new HtmlStringBuffer(contextPath.length() + href.length());
+
+                    // Append the context path
+                    buffer.append(contextPath);
+                    buffer.append(href);
+                    href = buffer.toString();
+                }
+            }
+        }
+        setAttribute("href", href);
+    }
+
+    /**
+     * Return the <tt>href</tt> attribute.
+     *
+     * @return the href attribute
+     */
+    public String getHref() {
+        return getAttribute("href");
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Render the HTML representation of the CSS import to the specified buffer.
+     *
+     * @param buffer the buffer to render output to
+     */
+    public void render(HtmlStringBuffer buffer) {
+        renderConditionalCommentPrefix(buffer);
+
+        buffer.elementStart(getTag());
+
+        buffer.appendAttribute("id", getId());
+        appendAttributes(buffer);
+
+        buffer.elementEnd();
+
+        renderConditionalCommentSuffix(buffer);
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     *
+     * @param o the object with which to compare this instance with
+     * @return true if the specified object is the same as this object
+     */
+    public boolean equals(Object o) {
+        if (getHref() == null) {
+            throw new IllegalStateException("'href' attribute is not defined.");
+        }
+
+        //1. Use the == operator to check if the argument is a reference to this object.
+        if (o == this) {
+            return true;
+        }
+
+        //2. Use the instanceof operator to check if the argument is of the correct type.
+        if (!(o instanceof CssImport)) {
+            return false;
+        }
+
+        //3. Cast the argument to the correct type.
+        CssImport that = (CssImport) o;
+
+        return getHref() == null ? that.getHref() == null
+            : getHref().equals(that.getHref());
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     *
+     * @return a hash code value for this object
+     */
+    public int hashCode() {
+        return new HashCodeBuilder(17, 37).append(getHref()).toHashCode();
+    }
+
+    // ------------------------------------------------ Package Private Methods
+
+    /**
+     * This operation is not supported because CSS imports is always unique
+     * based on their <tt>href</tt> attribute.
+     *
+     * @see HtmlHeader#setUnique(boolean)
+     *
+     * @param unique sets whether the Css import should be unique or not
+     */
+    void setUnique(boolean unique) {
+        throw new UnsupportedOperationException("CssImport is always"
+            + " unique based on the 'href' attribute");
+    }
+}

Added: incubator/click/trunk/click/framework/src/org/apache/click/util/HtmlHeader.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/HtmlHeader.java?rev=751785&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/HtmlHeader.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/HtmlHeader.java Mon Mar  9 18:20:16 2009
@@ -0,0 +1,593 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.click.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.click.Context;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Provides a base class for rendering the HEAD entries of an HTML page, for
+ * example JavaScript (&lt;script&gt;) and Cascading Stylesheets (&lt;link&gt;).
+ * <p/>
+ * Four HTML import types are supported by default. They are:
+ * <ul>
+ *   <li>{@link JavascriptImport}, for importing <tt>external</tt> JavaScript
+ *   using the &lt;script&gt; element.</li>
+ *   <li>{@link Javascript}, for including <tt>inline</tt> JavaScript
+ *   using the &lt;script&gt; element.</li>
+ *   <li>{@link CssImport}, for importing <tt>external</tt> Stylesheets
+ *   using the &lt;link&gt; element.</li>
+ *   <li>{@link Css}, for including <tt>inline</tt> Stylesheets using the
+ *   &lt;style&gt; element.</li>
+ * </ul>
+ *
+ * <h3>Remove duplicates</h3>
+ * Click will ensure that duplicate HtmlHeaders are removed by checking the
+ * {@link #isUnique()} property. No matter how many Controls or Pages
+ * import the same HtmlHeader, only one will be rendered if
+ * {@link #isUnique()} returns <tt>true</tt>.
+ * <p/>
+ * The rules for defining a unique HtmlHeader is as follows:
+ * <ul>
+ * <li>{@link JavascriptImport} and {@link CssImport} is unique based on the
+ * attributes {@link JavascriptImport#getSrc()} and {@link CssImport#getHref()}
+ * respectively.</li>
+ * <li>{@link Javascript} and {@link Css} is unique if their HTML
+ * {@link #setId(java.lang.String) ID} attribute is set. The HTML
+ * spec defines that an element's HTML ID must be unique per page.</li>
+ * </ul>
+ * For example:
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the JavaScript and CSS is only added
+ *         // the first time this method is called.
+ *         if (htmlHeaders== null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             JavascriptImport jsImport = new JavascriptImport("/js/mylib.js");
+ *             // Click will ensure the library "/js/mylib.js" is
+ *             // only included once in the Page
+ *             htmlHeaders.add(jsImport);
+ *
+ *             Javascript script = new Javascript("alert('Hello!');");
+ *             // Click won't ensure the script is unique because its ID
+ *             // attribute is not defined
+ *             htmlHeaders.add(script);
+ *
+ *             script = new Javascript("alert('Hello!');");
+ *             script.setId("my-unique-script-id");
+ *             // Click will ensure the script is unique because its ID attribute
+ *             // is defined. Click will remove other scripts with the same ID
+ *             htmlHeaders..add(script);
+ *
+ *             CssImport cssImport = new CssImport("/css/style.css");
+ *             // Click will ensure the library "/css/style.css" is
+ *             // only included once in the Page
+ *             htmlHeaders.add(cssImport);
+ *
+ *             Css css = new Css("body { font-weight: bold; }");
+ *             css.setId("my-unique-style-id");
+ *             // Click will ensure the css is unique because its ID attribute
+ *             // is defined. Click will remove other css styles with the same ID
+ *             htmlHeaders.add(css);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * <h3>Conditional comment support for Internet Explorer</h3>
+ *
+ * Sometimes it is necessary to provide additional Javascript and CSS for
+ * Internet Explorer because it deviates quite often from the standard.
+ * <p/>
+ * Conditional comments allows you to wrap the import in a special comment which
+ * only IE understands, meaning other browsers won't process the HtmlHeader.
+ * <p/>
+ * You can read more about conditional comments
+ * <a target="_blank" href="http://msdn.microsoft.com/en-us/library/ms537512(VS.85).aspx#syntax">here</a>
+ * and <a target="_blank" href="http://www.quirksmode.org/css/condcom.html">here</a>
+ * <p/>
+ * It has to be said that IE7 and up has much better support for CSS, thus
+ * conditional comments are mostly used for IE6 and below.
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the JavaScript and CSS is only added
+ *         // the first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             CssImport cssImport = new CssImport("/css/ie-style.css");
+ *             // Use one of the predefined conditional comments to target IE6
+ *             // and below
+ *             cssImport.setConditionalComment(IE_LESS_THAN_IE7);
+ *             htmlHeaders.add(cssImport);
+ *
+ *             cssImport = new CssImport("/css/ie-style2.css");
+ *             // Use a custom predefined conditional comments to target only IE6
+ *             cssImport.setConditionalComment("[if IE 6]");
+ *             htmlHeaders.add(cssImport);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * Click contains some predefined Conditional Comments namely
+ * {@link #IF_IE}, {@link #IF_LESS_THAN_IE7} and {@link #IF_IE7}.
+ *
+ * <h3>Character data (CDATA) support</h3>
+ *
+ * Sometimes it is necessary to wrap <tt>inline</tt> {@link Javascript} and
+ * {link Css} in CDATA tags. Two use cases are common for doing this:
+ * <ul>
+ * <li>For XML parsing: When using Ajax one often send back partial
+ * XML snippets to the browser, which is parsed as valid XML. However the XML
+ * parser will throw an error if the script contains special XML characters
+ * such as '&amp;', '&lt;' and '&gt;'. For these situations it is recommended
+ * to wrap the script content inside CDATA tags.
+ * </li>
+ * <li>XHTML validation: if you want to validate your site using an XHTML
+ * validator e.g: <a target="_blank" href="http://validator.w3.org/">http://validator.w3.org/</a>.</li>
+ * </ul>
+ *
+ * Here is an example of wrapping scripts and styles inside CDATA tags:
+ * <pre class="prettyprint">
+ * &lt;script type="text/javascript"&gt;
+ *  /&lowast;&lt;![CDATA[&lowast;/
+ *
+ *  if(x &lt; y) alert('Hello');
+ *
+ *  /&lowast;]]&gt;&lowast;/
+ * &lt;/script&gt;
+ *
+ * &lt;style type="text/css"&gt;
+ *  /&lowast;&lt;![CDATA[&lowast;/
+ *
+ *  div &gt; p {
+ *    border: 1px solid black;
+ *  }
+ *
+ *  /&lowast;]]&gt;&lowast;/
+ * &lt;/style&gt; </pre>
+ *
+ * Notice the CDATA tags are commented out which ensures older browsers that
+ * don't understand the CDATA tag, will ignore it and only process the actual
+ * content.
+ * <p/>
+ * For an overview of XHTML validation and CDATA tags please see
+ * <a target="_blank" href="http://javascript.about.com/library/blxhtml.htm">http://javascript.about.com/library/blxhtml.htm</a>.
+ *
+ * @author Bob Schellink
+ */
+public abstract class HtmlHeader {
+
+    // -------------------------------------------------------------- Constants
+
+    /**
+     * A predefined conditional comment to test if browser is IE. Value:
+     * <tt>[if IE]</tt>.
+     */
+    public static final String IF_IE = "[if IE]";
+
+    /**
+     * A predefined conditional comment to test if browser is less than IE7.
+     * Value: <tt>[if lt IE 7]</tt>.
+     */
+    public static final String IF_LESS_THAN_IE7 = "[if lt IE 7]";
+
+    /**
+     * A predefined conditional comment to test if browser is IE7. Value:
+     * <tt>[if IE 7]</tt>.
+     */
+    public static final String IF_IE7 = "[if IE 7]";
+
+    // -------------------------------------------------------------- Variables
+
+    /** The HtmlHeader attributes Map. */
+    private Map attributes;
+
+    /** The Internet Explorer conditional comment to wrap the HtmlHeader import with. */
+    private String conditionalComment;
+
+    /**
+     * Indicates if Click should ensure the import is unique, default value is
+     * <tt>false</tt>. <b>Note:</b> subclasses of HtmlHeader have different rules to
+     * determine if unique should be true or false.
+     */
+    private boolean unique = false;
+
+    /**
+     * Indicates if the HtmlHeader's content should be wrapped in a CDATA tag.
+     * <b>Note:</b> this property only applies to HtmlHeader imports which contain
+     * <tt>inline</tt> content.
+     */
+    private boolean characterData = false;
+
+    // ------------------------------------------------------ Public properties
+
+    /**
+     * Returns the HtmlHeader import HTML tag, the default value is <tt>null</tt>.
+     * <p/>
+     * Subclasses should override this method and return the correct tag.
+     *
+     * @return this HtmlHeader import HTML tag
+     */
+    public String getTag() {
+        return null;
+    }
+
+    /**
+     * Return true if the HtmlHeader's content should be wrapped in CDATA tags,
+     * false otherwise.
+     *
+     * @return true if the HtmlHeader's content should be wrapped in CDATA tags,
+     * false otherwise
+     */
+    public boolean isCharacterData() {
+        return characterData;
+    }
+
+    /**
+     * Sets whether the HtmlHeader's content should be wrapped in CDATA tags or not.
+     *
+     * @param characterData true indicates that the HtmlHeader's content should be
+     * wrapped in CDATA tags, false otherwise
+     */
+    public void setCharacterData(boolean characterData) {
+        this.characterData = characterData;
+    }
+
+    /**
+     * Return true if the HtmlHeader should be unique, false otherwise. The default
+     * value is <tt>true</tt> if the {@link #getId() ID} attribute is defined,
+     * false otherwise.
+     *
+     * @return true if the HtmlHeader should be unique, false otherwise.
+     */
+    public boolean isUnique() {
+        String id = getId();
+
+        // If id is defined, import will be any duplicate import found will be
+        // filtered out
+        if (StringUtils.isNotBlank(id)) {
+            return true;
+        }
+        return unique;
+    }
+
+    /**
+     * Return the "id" attribute value or null if no id is defined.
+     *
+     * @return HTML element identifier attribute "id" value or null if no id
+     * is defined
+     */
+    public String getId() {
+        return getAttribute("id");
+    }
+
+    /**
+     * Set the HTML id attribute for the import with the given value.
+     *
+     * @param id the element HTML id attribute value to set
+     */
+    public void setId(String id) {
+        if (id != null) {
+            setAttribute("id", id);
+        } else {
+            getAttributes().remove("id");
+        }
+    }
+
+    /**
+     * Return Internal Explorer's <tt>conditional comment</tt> to wrap the HtmlHeader
+     * import with.
+     *
+     * @return Internal Explorer's conditional comment to wrap the HtmlHeader import
+     * with.
+     */
+    public String getConditionalComment() {
+        return conditionalComment;
+    }
+
+    /**
+     * Set Internet Explorer's conditional comment to wrap the HtmlHeader import with.
+     *
+     * @param conditionalComment Internet Explorer's conditional comment to wrap
+     * the HtmlHeader import with
+     */
+    public void setConditionalComment(String conditionalComment) {
+        this.conditionalComment = conditionalComment;
+    }
+
+    /**
+     * Return the import HTML attribute with the given name, or null if the
+     * attribute does not exist.
+     *
+     * @param name the name of link HTML attribute
+     * @return the link HTML attribute
+     */
+     public String getAttribute(String name) {
+        if (hasAttributes()) {
+            return (String) getAttributes().get(name);
+        }
+        return null;
+    }
+
+    /**
+     * Set the import attribute with the given attribute name and value.
+     *
+     * @param name the attribute name
+     * @param value the attribute value
+     * @throws IllegalArgumentException if name parameter is null
+     */
+    public void setAttribute(String name, String value) {
+        if (name == null) {
+            throw new IllegalArgumentException("Null name parameter");
+        }
+
+        if (value != null) {
+            getAttributes().put(name, value);
+        } else {
+            getAttributes().remove(name);
+        }
+    }
+
+    /**
+     * Return the import attributes Map.
+     *
+     * @return the import attributes Map.
+     */
+    public Map getAttributes() {
+        if (attributes == null) {
+            attributes = new HashMap();
+        }
+        return attributes;
+    }
+
+    /**
+     * Return true if the import has attributes or false otherwise.
+     *
+     * @return true if the import has attributes on false otherwise
+     */
+    public boolean hasAttributes() {
+        if (attributes != null) {
+            return !attributes.isEmpty();
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns true if specified attribute is defined, false otherwise.
+     *
+     * @param name the specified attribute to check
+     * @return true if name is a defined attribute
+     */
+    public boolean hasAttribute(String name) {
+        return hasAttributes() && getAttributes().containsKey(name);
+    }
+
+    // --------------------------------------------------------- Public methods
+
+    /**
+     * Return the thread local Context.
+     *
+     * @return the thread local Context
+     */
+    public Context getContext() {
+        return Context.getThreadLocalContext();
+    }
+
+    /**
+     * Render the HTML representation of the HtmlHeader import to the specified buffer.
+     * <p/>
+     * If {@link #getTag()} returns null, this method will return an empty
+     * string.
+     *
+     * @param buffer the specified buffer to render the import output to
+     */
+    public void render(HtmlStringBuffer buffer) {
+        renderConditionalCommentPrefix(buffer);
+
+        if (getTag() == null) {
+            return;
+        }
+        renderTagBegin(getTag(), buffer);
+        renderTagEnd(getTag(), buffer);
+
+        renderConditionalCommentSuffix(buffer);
+    }
+
+    /**
+     * Return the HTML string representation of the Html import.
+     *
+     * @return the HTML string representation of the Html import
+     */
+    public String toString() {
+        if (getTag() == null) {
+            return "";
+        }
+        HtmlStringBuffer buffer = new HtmlStringBuffer(getImportSizeEst());
+        render(buffer);
+        return buffer.toString();
+    }
+
+    // ------------------------------------------------ Package Private Methods
+
+    /**
+     * Render the specified {@link #getTag() tag} and {@link #getAttributes()}.
+     * <p/>
+     * <b>Please note:</b> the tag will not be closed by this method. This
+     * enables callers of this method to append extra attributes as needed.
+     * <p/>
+     * For example the result of calling:
+     * <pre class="prettyprint">
+     * Field field = new TextField("mytext");
+     * HtmlStringBuffer buffer = new HtmlStringBuffer();
+     * field.renderTagBegin("div", buffer);
+     * </pre>
+     * will be:
+     * <pre class="prettyprint">
+     * &lt;div name="mytext" id="mytext"
+     * </pre>
+     * Note that the tag is not closed.
+     *
+     * @param tagName the name of the tag to render
+     * @param buffer the buffer to append the output to
+     */
+    void renderTagBegin(String tagName, HtmlStringBuffer buffer) {
+        if (tagName == null) {
+            throw new IllegalStateException("Tag cannot be null");
+        }
+
+        buffer.elementStart(tagName);
+
+        buffer.appendAttribute("id", getId());
+        appendAttributes(buffer);
+    }
+
+    /**
+     * Closes the specified {@link #getTag() tag}.
+     *
+     * @param tagName the name of the tag to close
+     * @param buffer the buffer to append the output to
+     */
+    void renderTagEnd(String tagName, HtmlStringBuffer buffer) {
+        buffer.elementEnd();
+    }
+
+    /**
+     * Return the estimated rendered import size in characters.
+     *
+     * @return the estimated rendered import size in characters
+     */
+    int getImportSizeEst() {
+        int size = 0;
+        if (getTag() != null && hasAttributes()) {
+            //length of the markup -> </> == 3
+            //1 * tag.length()
+            size += 3 + getTag().length();
+            //using 20 as an estimate
+            size += 20 * getAttributes().size();
+        }
+        return size;
+    }
+
+    /**
+     * Append all the HtmlHeader import attributes to the specified buffer.
+     *
+     * @param buffer the specified buffer to append all the attributes
+     */
+    void appendAttributes(HtmlStringBuffer buffer) {
+        if (hasAttributes()) {
+            buffer.appendAttributes(attributes);
+        }
+    }
+
+    /**
+     * Render the CDATA tag prefix to the specified buffer if
+     * {@link #isCharacterData()} returns true. The default value is
+     * <tt>/&lowast;&lt;![CDATA[&lowast;/</tt>.
+     *
+     * @param buffer buffer to append the conditional comment prefix
+     */
+    void renderCharacterDataPrefix(HtmlStringBuffer buffer) {
+        // Wrap character data in CDATA block
+        if (isCharacterData()) {
+            buffer.append("/*<![CDATA[*/ ");
+        }
+    }
+
+    /**
+     * Render the CDATA tag suffix to the specified buffer if
+     * {@link #isCharacterData()} returns true. The default value is
+     * <tt>/&lowast;]]&gt;&lowast;/</tt>.
+     *
+     * @param buffer buffer to append the conditional comment prefix
+     */
+     void renderCharacterDataSuffix(HtmlStringBuffer buffer) {
+         if (isCharacterData()) {
+             buffer.append(" /*]]>*/");
+         }
+    }
+
+    /**
+     * Render the {@link #getConditionalComment() conditional comment} prefix
+     * to the specified buffer. If the conditional comment is not defined this
+     * method won't append to the buffer.
+     *
+     * @param buffer buffer to append the conditional comment prefix
+     */
+    void renderConditionalCommentPrefix(HtmlStringBuffer buffer) {
+        String conditional = getConditionalComment();
+
+        // Render IE conditional comment
+        if (StringUtils.isNotBlank(conditional)) {
+            buffer.append("<!--").append(conditional).append(">\n");
+        }
+    }
+
+    /**
+     * Render the {@link #getConditionalComment() conditional comment} suffix
+     * to the specified buffer. If the conditional comment is not defined this
+     * method won't append to the buffer.
+     *
+     * @param buffer buffer to append the conditional comment suffix
+     */
+    void renderConditionalCommentSuffix(HtmlStringBuffer buffer) {
+        String conditional = getConditionalComment();
+
+        // Close IE conditional comment
+        if (StringUtils.isNotBlank(conditional)) {
+            buffer.append("\n<![endif]-->");
+        }
+    }
+
+    /**
+     * This method provides backwards compatibility with the String based
+     * HTML imports, to indicate whether Javascript and CSS must be unique
+     * or not. The replacement functionality is provided by the html
+     * {@link #setId(java.lang.String) ID} attribute.
+     * <p/>
+     * This property is *not* for public use and will be removed in a future
+     * release. This property is only set from PageImports.
+     *
+     * @deprecated use {@link #setId(java.lang.String)} instead
+     *
+     * @param unique sets whether the HtmlHeader import should be unique or not
+     */
+    void setUnique(boolean unique) {
+        String id = getId();
+
+        // If id is defined, unique property cannot be set to false
+        if (StringUtils.isNotBlank(id) && !unique) {
+            throw new IllegalArgumentException("Cannot set unique property"
+                + " to 'false' because an 'ID' attribute has been defined"
+                + " which indicates the import should be unique.");
+        }
+        this.unique = unique;
+    }
+}

Added: incubator/click/trunk/click/framework/src/org/apache/click/util/Javascript.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/Javascript.java?rev=751785&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/Javascript.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/Javascript.java Mon Mar  9 18:20:16 2009
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.click.util;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * Provides a HEAD entry for importing <tt>inline</tt> Javascript through the
+ * &lt;script&gt; element.
+ * <p/>
+ * Example usage:
+ *
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the JS is only added the
+ *         // first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             Javascript javascript = new Javascript("alert('Hello World!);");
+ *             htmlHeaders.add(javascript);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>javascript</tt> instance will render the output:
+ *
+ * <pre class="prettyprint">
+ * &lt;script type="text/javascript"&gt;
+ * alert('Hello World');
+ * &lt;/script&gt;
+ * </pre>
+ *
+ * Below is an example showing how to create inline Javascript from a
+ * Velocity template.
+ * <p/>
+ * First we create a Velocity template <tt>(/js/mycorp-template.js)</tt> which
+ * contains the variable <tt>$divId</tt> that must be replaced at runtime by
+ * the real Div ID attribute:
+ *
+ * <pre class="prettyprint">
+ * hide = function() {
+ *     var div = document.getElementById('$divId');
+ *     div.style.display = "none";
+ * }
+ * </pre>
+ *
+ * Next is the Page implementation:
+ *
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the JS is only added the
+ *         // first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             // Create a default template model to pass to the template
+ *             Map model = ClickUtils.createTemplateModel(this, getContext());
+ *
+ *             // Add the id of the div to hide
+ *             model.put("divId", "myDiv");
+ *
+ *             // Specify the path to JAvascript Velocity template
+ *             String jsTemplate = "/js/mycorp-template.js";
+ *
+ *             // Render the template providing it with the model
+ *             String template = getContext().renderTemplate(jsTemplate, model);
+ *
+ *             // Create the inline Javascript for the given template
+ *             Javascript content = new Javascript(template);
+ *             htmlHeaders.add(content);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>javascript</tt> above will render as follows (assuming the context
+ * path is <tt>myApp</tt>):
+ *
+ * <pre class="prettyprint">
+ * &lt;script type="text/javascript"&gt;
+ *     hide = function() {
+ *         var div = document.getElementById('myDiv');
+ *         div.style.display = "none";
+ *     }
+ * &lt;/style&gt;
+ * </pre>
+ *
+ * @author Bob Schellink
+ */
+public class Javascript extends HtmlHeader {
+
+    // -------------------------------------------------------------- Constants
+
+    /** A buffer holding the inline CSS content. */
+    private HtmlStringBuffer content = new HtmlStringBuffer();
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * Construct a new inline Javascript element.
+     */
+    public Javascript() {
+        this(null);
+    }
+
+    /**
+     * Construct a new inline Javascript element with the given content.
+     *
+     * @param content the Javascript content
+     */
+    public Javascript(String content) {
+        if (content != null) {
+            this.content.append(content);
+        }
+        setAttribute("type", "text/javascript");
+    }
+
+    // ------------------------------------------------------ Public Properties
+
+    /**
+     * Returns the Javascript HTML tag: &lt;script&gt;.
+     *
+     * @return the Javascript HTML tag: &lt;script&gt;
+     */
+    public String getTag() {
+        return "script";
+    }
+
+    /**
+     * Return the Javascript content buffer.
+     *
+     * @return the Javascript content buffer
+     */
+    public HtmlStringBuffer getContent() {
+        return content;
+    }
+
+    /**
+     * Set the Javascript content buffer.
+     *
+     * @param content the new content buffer
+     */
+    public void setContent(HtmlStringBuffer content) {
+        this.content = content;
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Append the given Javascript string to the content buffer.
+     *
+     * @param content the Javascript string to append to the content
+     * buffer
+     */
+    public void append(String content) {
+        this.content.append(content);
+    }
+
+    /**
+     * Render the HTML representation of the JavaScript to the specified
+     * buffer.
+     *
+     * @param buffer the buffer to render output to
+     */
+    public void render(HtmlStringBuffer buffer) {
+
+        // Render IE conditional comment if conditional comment was set
+        renderConditionalCommentPrefix(buffer);
+
+        buffer.elementStart(getTag());
+
+        buffer.appendAttribute("id", getId());
+        appendAttributes(buffer);
+
+        buffer.closeTag();
+
+        // Render CDATA tag if necessary
+        renderCharacterDataPrefix(buffer);
+
+        buffer.append(getContent());
+
+        renderCharacterDataSuffix(buffer);
+
+        buffer.elementEnd(getTag());
+
+        renderConditionalCommentSuffix(buffer);
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     *
+     * @param o the object with which to compare this instance with
+     * @return true if the specified object is the same as this object
+     */
+    public boolean equals(Object o) {
+        if (!isUnique()) {
+            return super.equals(o);
+        }
+
+        //1. Use the == operator to check if the argument is a reference to this object.
+        if (o == this) {
+            return true;
+        }
+
+        //2. Use the instanceof operator to check if the argument is of the correct type.
+        if (!(o instanceof Javascript)) {
+            return false;
+        }
+
+        //3. Cast the argument to the correct type.
+        Javascript that = (Javascript) o;
+
+        String id = getId();
+        String thatId = that.getId();
+        return id == null ? thatId == null : id.equals(thatId);
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     *
+     * @return a hash code value for this object
+     */
+    public int hashCode() {
+        if (!isUnique()) {
+            return super.hashCode();
+        }
+        return new HashCodeBuilder(17, 37).append(getId()).toHashCode();
+    }
+
+    // ------------------------------------------------ Package Private Methods
+
+    /**
+     * @see HtmlHeader#setUnique(boolean)
+     *
+     * @deprecated use {@link #setId(java.lang.String)} instead
+     *
+     * @param unique sets whether the HtmlHeader import should be unique or not
+     */
+    void setUnique(boolean unique) {
+        super.setUnique(unique);
+
+        // If CSS is unique and ID is not defined, derive the ID from the content
+        if (unique && StringUtils.isBlank(getId()) && getContent().length() > 0) {
+            int hash = Math.abs(getContent().toString().hashCode());
+            setId(Integer.toString(hash));
+        }
+    }
+}

Added: incubator/click/trunk/click/framework/src/org/apache/click/util/JavascriptImport.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/util/JavascriptImport.java?rev=751785&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/util/JavascriptImport.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/util/JavascriptImport.java Mon Mar  9 18:20:16 2009
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.click.util;
+
+import org.apache.click.Context;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+
+/**
+ * Provides a JavaScript HEAD entry for importing <tt>external</tt> JavaScripts
+ * through the &lt;script&gt; element.
+ * <p/>
+ * Example usage:
+ * <pre class="prettyprint">
+ * public class MyPage extends Page {
+ *
+ *     public List getHtmlHeaders() {
+ *         // We use lazy loading to ensure the JS import is only added the
+ *         // first time this method is called.
+ *         if (htmlHeaders == null) {
+ *             // Get the header entries from the super implementation
+ *             htmlHeaders = super.getHtmlHeaders();
+ *
+ *             JavascriptImport jsImport = new JavascriptImport("/js/js-library.js");
+ *             htmlHeaders.add(jsImport);
+ *         }
+ *         return htmlHeaders;
+ *     }
+ * } </pre>
+ *
+ * The <tt>jsImport</tt> above will be rendered as follows (assuming the context
+ * path is <tt>myApp</tt>):
+ * <pre class="prettyprint">
+ * &lt;script type="text/javascript" href="/myApp/js/js-library.js"&gt;&lt;/script&gt; </pre>
+ *
+ * @author Bob Schellink
+ */
+public class JavascriptImport extends HtmlHeader {
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * Constructs a new JavascriptImport.
+     */
+    public JavascriptImport() {
+        this(null);
+    }
+
+    /**
+     * Construct a new JavascriptImport with the specified <tt>src</tt> attribute.
+     * <p/>
+     * <b>Please note</b> if the given <tt>src</tt> begins with a <tt class="wr">"/"</tt>
+     * character the src will be prefixed with the web application
+     * <tt>context path</tt>.
+     *
+     * @param src the Javascript src attribute
+     */
+    public JavascriptImport(String src) {
+        setSrc(src);
+        setAttribute("type", "text/javascript");
+    }
+
+    // ------------------------------------------------------ Public Properties
+
+    /**
+     * Returns the Css import HTML tag: &lt;script&gt;.
+     *
+     * @return the Css import HTML tag: &lt;script&gt;
+     */
+    public String getTag() {
+        return "script";
+    }
+
+    /**
+     * This method always return true because JavaScript import must be unique
+     * based on its <tt>src</tt> attribute. In other words the Page HEAD should
+     * only contain a single JavaScript import for the specific <tt>src</tt>.
+     *
+     * @see HtmlHeader#isUnique()
+     *
+     * @return true because JavaScript import must unique based on its
+     * <tt>src</tt> attribute
+     */
+    public boolean isUnique() {
+        return true;
+    }
+
+    /**
+     * Sets the <tt>src</tt> attribute.
+     * <p/>
+     * If the given <tt>src</tt> begins with a <tt class="wr">"/"</tt> character
+     * the sr will be prefixed with the web application <tt>context path</tt>.
+     * Note if the given src is already prefixed with the <tt>context path</tt>,
+     * Click won't add it a second time.
+     *
+     * @param src the new src attribute
+     */
+    public void setSrc(String src) {
+        if (src != null) {
+            if (src.charAt(0) == '/') {
+                Context context = getContext();
+                String contextPath = context.getRequest().getContextPath();
+
+                // Guard against adding duplicate context path
+                if (!src.startsWith(contextPath + '/')) {
+                    HtmlStringBuffer buffer =
+                        new HtmlStringBuffer(contextPath.length() + src.length());
+
+                    // Append the context path
+                    buffer.append(contextPath);
+                    buffer.append(src);
+                    src = buffer.toString();
+                }
+            }
+        }
+        setAttribute("src", src);
+    }
+
+    /**
+     * Return the <tt>src</tt> attribute.
+     *
+     * @return the src attribute
+     */
+    public String getSrc() {
+        return getAttribute("src");
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Render the HTML representation of the JavaScript import to the specified
+     * buffer.
+     *
+     * @param buffer the buffer to render output to
+     */
+    public void render(HtmlStringBuffer buffer) {
+        renderConditionalCommentPrefix(buffer);
+
+        buffer.elementStart(getTag());
+
+        buffer.appendAttribute("id", getId());
+        appendAttributes(buffer);
+
+        buffer.closeTag();
+
+        buffer.elementEnd(getTag());
+
+        renderConditionalCommentSuffix(buffer);
+    }
+
+    /**
+     * @see java.lang.Object#equals(java.lang.Object)
+     *
+     * @param o the object with which to compare this instance with
+     * @return true if the specified object is the same as this object
+     */
+    public boolean equals(Object o) {
+        //1. Use the == operator to check if the argument is a reference to this object.
+        if (o == this) {
+            return true;
+        }
+
+        //2. Use the instanceof operator to check if the argument is of the correct type.
+        if (!(o instanceof JavascriptImport)) {
+            return false;
+        }
+
+        //3. Cast the argument to the correct type.
+        JavascriptImport that = (JavascriptImport) o;
+
+        return getSrc() == null ? that.getSrc() == null
+            : getSrc().equals(that.getSrc());
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     *
+     * @return a hash code value for this object
+     */
+    public int hashCode() {
+        return new HashCodeBuilder(17, 37).append(getSrc()).toHashCode();
+    }
+
+    // ------------------------------------------------ Package Private Methods
+
+    /**
+     * This operation is not supported because JavaScript imports is always
+     * unique based on their <tt>src</tt> attribute.
+     *
+     * @see HtmlHeader#setUnique(boolean)
+     *
+     * @param unique sets whether the JavaScript import should be unique or not
+     */
+    void setUnique(boolean unique) {
+        throw new UnsupportedOperationException("JavascriptImport is always"
+            + " unique based on the 'src' attribute");
+    }
+}