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 (&nbsp;). 245 */ 246 public static Label newLabelNBSP( String id ) 247 { 248 final Label label = new Label( id, " " ); 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}