001package votorola.a.web.wic; // Copyright 2008-2012, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import javax.servlet.http.*;
004import org.apache.wicket.*;
005import org.apache.wicket.behavior.AttributeAppender;
006import org.apache.wicket.markup.MarkupType;
007import org.apache.wicket.markup.html.IHeaderResponse;
008import org.apache.wicket.markup.html.basic.*;
009import org.apache.wicket.markup.html.form.*;
010import org.apache.wicket.markup.html.navigation.paging.*;
011import org.apache.wicket.markup.repeater.data.DataView;
012import org.apache.wicket.request.http.WebRequest;
013import org.apache.wicket.request.http.WebResponse;
014import org.apache.wicket.request.mapper.parameter.PageParameters;
015import org.apache.wicket.model.*;
016import org.apache.wicket.util.time.Duration;
017import votorola.a.*;
018import votorola.a.web.wic.authen.*;
019import votorola.a.voter.*;
020import votorola.g.lang.*;
021import votorola.g.web.*;
022import votorola.g.web.wic.*;
023
024
025/** An HTML page in the Wicket web interface.  The page defines the following JavaScript
026  * variables for the use of client scripts:
027  *
028  * <table class='definition' style='margin-left:1em'>
029  *     <tr>
030  *         <th class='key'>Name</th>
031  *         <th>Value</th>
032  *         </tr>
033  *     <tr><td class='key'>voc.pageJClass</td>
034  *
035  *         <td>The class name of the specific Java type that implements this page.  The
036  *         name is fully qualified.</td>
037  *
038  *         </tr>
039  *     </table>
040  */
041public abstract @ThreadRestricted("wicket") class VPageHTML extends VPage
042{
043
044
045    /** Constructs a VPageHTML, per {@linkplain
046      * org.apache.wicket.markup.html.WebPage#WebPage() WebPage}().
047      */
048    protected VPageHTML() {}
049
050
051
052    /** Constructs a VPageHTML, per {@linkplain
053      * org.apache.wicket.markup.html.WebPage#WebPage(PageParameters) WebPage}(_pP).
054      */
055    protected VPageHTML( PageParameters _pP ) { super( _pP ); }
056
057
058
059   // ------------------------------------------------------------------------------------
060
061
062    /** Adds a cookie to convey the user's login state to cacheable pages.  Client side
063      * script <a href='../../../../../web/context/web/VPageHTML.js' target='_top'>VPageHTML.js</a>
064      * compares the value of this cookie against that
065      * encoded in the page and automatically reloads the page to correct the rendering
066      * when necessary.  This applies only to cacheable pages and is employed as a
067      * temporary workaround pending the movement of login dependent rendering to the
068      * client side.
069      *
070      *     @param contextPath the context path of the request, which will also be used as
071      *       the cookie path.
072      *
073      *     @see #encodeSessionStateCookieValue(votorola.a.web.wic.VSession.User)
074      *     @see #COOKIE_SESSION_STATE
075      */
076    static void addSessionStateCookie( final String encodedValue, final String contextPath,
077      final WebResponse resW )
078    {
079        final CookieX cookie = new CookieX( COOKIE_SESSION_STATE, encodedValue, /*toEncode*/false );
080        cookie.setMaxAge( CookieX.DURATION_YEAR_S ); // only cost of loss is browser does a refresh
081        cookie.setPath( contextPath ); // allow access from pages other than the current one
082        WebResponseX.addCookie( resW, cookie );
083    }
084
085
086
087    /** Appends a CSS class identifier to the component's 'class' attribute.  Uses an
088      * AttributeAppender for this purpose.  If you wish to remove the identifier later,
089      * then use your own AttributeAppender and override its isEnabled() method.
090      *
091      *     @return the component
092      */
093    public static <C extends Component> C appendStyleClass( final C component,
094      final String classIdentifier )
095    {
096        component.add( new AttributeAppender( "class", new Model<String>(classIdentifier), " " ));
097        return component;
098    }
099
100
101
102    /** The name of the cookie for transmitting the state of the session to cacheable
103      * pages.
104      *
105      *     @see #addSessionStateCookie(String,String,WebResponse)
106      */
107    static final String COOKIE_SESSION_STATE = "vo_sessionState";
108
109
110
111    /** Encodes a value suitable for a session state cookie.
112      *
113      *     @see #addSessionStateCookie(String,String,WebResponse)
114      */
115    static String encodeSessionStateCookieValue( final VSession.User user )
116    {
117        return user == null? "out": CookieX.encodedValue( user.username() );
118          // login state is all that cacheable pages depend on
119    }
120
121
122
123    /** The duration for client-side caching, where enabled.  The default value of one
124      * hour is suitable for pages that contain voting results.
125      *
126      *     @see #isCacheable()
127      *     @see #setCacheDuration(Duration)
128      */
129    protected final Duration getCacheDuration() { return cacheDuration; }
130
131
132        private Duration cacheDuration = Duration.ONE_HOUR;
133
134
135        /** A duration of roughly one year, which is suitable as a cache duration for
136          * pages that have stable content.
137          *
138          *     @see #getCacheDuration()
139          */
140        public static final Duration CACHE_DURATION_YEAR = Duration.days( 365 );
141
142
143        /** Sets the duration for client-side caching, where enabled.
144          *
145          *     @see #getCacheDuration()
146          *     @see #CACHE_DURATION_YEAR
147          */
148        protected final void setCacheDuration( final Duration d ) { cacheDuration = d; }
149
150
151
152    /** The location of the page icon.
153      *
154      *     @see VOWicket#defaultPageIcon()
155      */
156    protected final String getPageIcon() { return pageIcon; }
157
158
159        private String pageIcon = VOWicket.get().defaultPageIcon().toString();
160
161
162        /** Sets the location of the page icon.
163          */
164        protected final void setPageIcon( String s ) { pageIcon = s; }
165
166
167
168    /** Constrains a CharSequence text field to the standard input length.  Wraps a length
169      * limiter around its model, and adds a 'maxLength' attribute to its view.  For use
170      * only with a field that is backed by a character sequence model.  (For others, the
171      * associated IConverter should enforce its own constraints at conversion time.)
172      *
173      *     @return the same field
174      *
175      *     @see ModelLengthLimiter
176      *     @see VoterInputTable#MAX_INPUT_LENGTH
177      *     @see #inputLengthValidator()
178      */
179    public static <S extends CharSequence, C extends TextField<S>> C inputLengthConstrained(
180      final C field )
181    {
182        field.setModel( new ModelLengthLimiter<S>(
183          field.getModel(), VoterInputTable.MAX_INPUT_LENGTH ));
184        field.add( AttributeModifier.replace( "maxlength",
185          Integer.toString( VoterInputTable.MAX_INPUT_LENGTH )));
186        return field;
187    }
188
189
190
191    /** Trains the form component to aquire the style class 'invalid', whenever it fails
192      * validation.
193      *
194      *     @return the component
195      */
196    public static <C extends FormComponent<?>> C invalidStyled( final C component )
197    {
198        component.add( invalidStyler );
199        return component;
200    }
201
202
203        /** For use on a FormComponent, only.
204          */
205        private static final AttributeAppender invalidStyler =
206          new AttributeAppender( "class", new Model<String>("invalid"), " " )
207        {
208            public @Override boolean isEnabled( final Component component )
209            {
210                return super.isEnabled(component) && !((FormComponent)component).isValid();
211            }
212        };
213
214
215
216    /** Answers whether client-side caching is enabled.  When enabled, the response will
217      * allow the client to cache this page privately for the cache duration; otherwise it
218      * will forbid caching.  The default value is false.
219      *
220      * <p>Cacheable pages may depend on the user's login state.  A {@linkplain
221      * #addSessionStateCookie(String,String,WebResponse) cookie/script mechanism} is used
222      * to automatically reload cached pages and bypass the cache following any change to
223      * login state.  The rendering of the page may therefore depend on the identity of
224      * the authenticated user, but not on any other crucial variable that might change over
225      * the course of the cache duration.</p>
226      *
227      *     @see #setCacheable(boolean)
228      *     @see #getCacheDuration()
229      */
230    protected final boolean isCacheable() { return isCacheable; }
231
232
233        private boolean isCacheable;
234
235
236        /** Sets whether client-side caching is enabled.
237          *
238          *     @see #isCacheable()
239          */
240        protected final void setCacheable( final boolean is ) { isCacheable = is; }
241
242
243
244    /** Constructs a label containing a non-breaking space character (&amp;nbsp;).
245      */
246    public static Label newLabelNBSP( String id )
247    {
248        final Label label = new Label( id, "&nbsp;" );
249        label.setEscapeModelStrings( false );
250        return label;
251    }
252
253
254
255    /** Constructs a label containing a non-breaking space character, for situations that
256      * preclude using a proper null component.  One such situation is the end of the
257      * page, where a null component somehow causes clipping of the content above (IE7).
258      */
259    public static Component newNullComponentAsLabel( String id ) { return newLabelNBSP( id ); }
260
261
262
263    /** Constructs a paging navigator with a standard configuration.
264      */
265    public static PagingNavigator newPagingNavigator( final String id, final DataView<?> dataView )
266    {
267        final PagingNavigator navigator = new PagingNavigator( id, dataView )
268     // navigator.getPagingNavigation().setViewSize( 5 );
269     //// getPagingNavigation() is null till after onBeforeRender() (since upgrade from 1.3.2 to 1.3.7)
270        {
271            protected PagingNavigation newNavigation( final String idPN, final IPageable pageable,
272              final IPagingLabelProvider labelProvider )
273            {
274                final PagingNavigation pN = super.newNavigation( idPN, pageable, labelProvider );
275                pN.setViewSize( 5 ); // page link short cuts, at a time (rather than default 10)
276                return pN;
277            }
278        };
279        return navigator;
280    }
281
282
283
284   // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------
285
286
287    public @Override void renderHead( final IHeaderResponse r )
288    {
289        assert MarkupType.HTML_MARKUP_TYPE.equals( getMarkupType() );
290        final VRequestCycle cycle = VRequestCycle.get();
291        final WebRequest reqW = cycle.vRequest();
292        final String contextPath = reqW.getContextPath();
293
294      // Define JavaScript variables for general use on client side ...
295      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
296        final StringBuilder b = new StringBuilder();
297        b.append(
298            "var voc =" // namespace for public, client-side variables
299          + "{" );
300        b.append( "pageJClass:'" ).append( getClass().getName() ).append( '\'' );
301
302      // ... and specifically for use in VPageHTML.js.
303      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
304        final String state;
305        b.append( ",contextPath:'" ).append( contextPath ).append( '\'' );
306        if( isCacheable )
307        {
308            final VSession.User user = VSession.get().user();
309            state = encodeSessionStateCookieValue( user );
310            final Cookie cookie = reqW.getCookie( COOKIE_SESSION_STATE );
311            if( cookie != null && "refreshing".equals(cookie.getValue()) )
312            {
313                // clear the loop guard, per VPageHTML.js
314                addSessionStateCookie( state, contextPath, cycle.vResponse() );
315            }
316            final Authenticator authenticator = VOWicket.get().authenticator();
317            if( authenticator instanceof WikiAuthenticator )
318            {
319                if( user != null && user.isPersistent() ) b.append( ",isPersistentLogin:true" );
320                b.append( ",pollwikiCookiePrefix:'" ).append(
321                  ((WikiAuthenticator)authenticator).getCookiePrefix() ).append( '\'' );
322            }
323        }
324        else state = "ignore"; // tell VPageHTML.js not to bother synchronizing this page
325        b.append( ",sessionState:'" ).append( state ).append( '\'' );
326        b.append( "};" );
327        r.renderJavaScript( b.toString(), /*unique ID for rendering*/VPageHTML.class.getName() );
328
329      // Inject VPageHTML.js ASAP, because it may force a reload of the page.
330      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
331        r.renderJavaScriptReference( cycle.staticContextLocation() + "/web/VPageHTML.js" );
332
333      // Ensure Wicket provides its Mootools-like extended 'domready' event.
334      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
335        r.renderOnDomReadyJavaScript( "/* forcing inclusion of wicket-event.js */" ); // resources/org.apache.wicket.markup.html.WicketEventReference/wicket-event.js
336
337      // Add a page icon.
338      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
339        r.renderString(
340            "<link rel='icon' type='image/png'\n"             // Mozilla
341          + " href='" + pageIcon + "'/>\n"
342       // + "<link rel='shortcut icon' type='image/x-icon'\n" // IE
343       // + " href='" + defaultDefaultPageIcon + "'/>\n"
344       /// fails, I get an IE default logo
345          );
346
347      // - - -
348        final String htmlHeaderInsert = vApplication().htmlHeaderInsert();
349        if( htmlHeaderInsert != null ) r.renderString( htmlHeaderInsert );
350    }
351
352
353
354   // - W e b - P a g e ------------------------------------------------------------------
355
356
357    /** Sets headers to enable or disable caching.
358      *
359      *     @see #isCacheable()
360      */
361    protected @Override final void setHeaders( final WebResponse r )
362    {
363        if( isCacheable )
364        {
365            r.enableCaching( cacheDuration, WebResponse.CacheScope.PRIVATE );
366            // This alone does not enable caching. As noted in WebResponseX, we must also
367            // avoid calling super.setHeaders(r).
368        }
369        else super.setHeaders( r ); // disables caching, as documented
370    }
371
372
373}