001: /*
002: * $Id: WebPage.java 5399 2006-04-17 10:23:38Z joco01 $ $Revision: 524366 $ $Date:
003: * 2006-04-17 12:23:38 +0200 (Mo, 17 Apr 2006) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket.markup.html;
019:
020: import java.io.Serializable;
021: import java.util.HashSet;
022: import java.util.Set;
023:
024: import org.apache.commons.logging.Log;
025: import org.apache.commons.logging.LogFactory;
026:
027: import wicket.Component;
028: import wicket.IRequestTarget;
029: import wicket.MetaDataKey;
030: import wicket.Page;
031: import wicket.PageMap;
032: import wicket.PageParameters;
033: import wicket.ResourceReference;
034: import wicket.Response;
035: import wicket.Session;
036: import wicket.behavior.AbstractBehavior;
037: import wicket.markup.ComponentTag;
038: import wicket.markup.MarkupElement;
039: import wicket.markup.MarkupStream;
040: import wicket.markup.TagUtils;
041: import wicket.markup.html.internal.HtmlBodyContainer;
042: import wicket.markup.html.link.BookmarkablePageLink;
043: import wicket.markup.parser.filter.BodyOnLoadHandler;
044: import wicket.markup.parser.filter.HtmlHeaderSectionHandler;
045: import wicket.model.IModel;
046: import wicket.model.Model;
047: import wicket.protocol.http.WebRequestCycle;
048: import wicket.protocol.http.WebResponse;
049: import wicket.protocol.http.request.urlcompressing.URLCompressor;
050: import wicket.protocol.http.request.urlcompressing.WebURLCompressingCodingStrategy;
051: import wicket.protocol.http.request.urlcompressing.WebURLCompressingTargetResolverStrategy;
052: import wicket.request.target.component.BookmarkablePageRequestTarget;
053: import wicket.request.target.component.IBookmarkablePageRequestTarget;
054: import wicket.util.lang.Objects;
055: import wicket.util.string.JavascriptUtils;
056:
057: /**
058: * Base class for HTML pages. This subclass of Page simply returns HTML when
059: * asked for its markup type. It also has a method which subclasses can use to
060: * retrieve a bookmarkable link to the application's home page.
061: * <p>
062: * WebPages can be constructed with any constructor when they are being used in
063: * a Wicket session, but if you wish to link to a Page using a URL that is
064: * "bookmarkable" (which implies that the URL will not have any session
065: * information encoded in it, and that you can call this page directly without
066: * having a session first directly from your browser), you need to implement
067: * your Page with a no-arg constructor or with a constructor that accepts a
068: * PageParameters argument (which wraps any query string parameters for a
069: * request). In case the page has both constructors, the constructor with
070: * PageParameters will be used.
071: *
072: * @author Jonathan Locke
073: * @author Eelco Hillenius
074: * @author Juergen Donnerstag
075: * @author Gwyn Evans
076: */
077: public class WebPage extends Page implements INewBrowserWindowListener {
078: private static final long serialVersionUID = 1L;
079:
080: /** log. */
081: private static final Log log = LogFactory.getLog(WebPage.class);
082:
083: /** meta data key for missing body tags logging. */
084: private static final MetaDataKey PAGEMAP_ACCESS_MDK = new MetaDataKey(
085: PageMapAccessMetaData.class) {
086: private static final long serialVersionUID = 1L;
087: };
088:
089: /**
090: * meta data for recording map map access.
091: */
092: private static final class PageMapAccessMetaData implements
093: Serializable {
094: private static final long serialVersionUID = 1L;
095:
096: Set pageMapNames = new HashSet(1);
097: }
098:
099: /** The resource references used for new window/tab support */
100: private static ResourceReference cookiesResource = new ResourceReference(
101: WebPage.class, "cookies.js");
102:
103: /** The body container */
104: private BodyContainer bodyContainer;
105:
106: /**
107: * The url compressor that will compress the urls by collapsing the
108: * component path and listener interface
109: */
110: private URLCompressor compressor;
111:
112: /**
113: * Constructor. Having this constructor public means that your page is
114: * 'bookmarkable' and hence can be called/ created from anywhere.
115: */
116: protected WebPage() {
117: commonInit();
118: }
119:
120: /**
121: * @see Page#Page(IModel)
122: */
123: protected WebPage(final IModel model) {
124: super (model);
125: commonInit();
126: }
127:
128: /**
129: * @see Page#Page(PageMap)
130: */
131: protected WebPage(final PageMap pageMap) {
132: super (pageMap);
133: commonInit();
134: }
135:
136: /**
137: * @see Page#Page(PageMap, IModel)
138: */
139: protected WebPage(final PageMap pageMap, final IModel model) {
140: super (pageMap, model);
141: commonInit();
142: }
143:
144: /**
145: * Constructor which receives wrapped query string parameters for a request.
146: * Having this constructor public means that your page is 'bookmarkable' and
147: * hence can be called/ created from anywhere. For bookmarkable pages (as
148: * opposed to when you construct page instances yourself, this constructor
149: * will be used in preference to a no-arg constructor, if both exist. Note
150: * that nothing is done with the page parameters argument. This constructor
151: * is provided so that tools such as IDEs will include it their list of
152: * suggested constructors for derived classes.
153: *
154: * @param parameters
155: * Wrapped query string parameters.
156: */
157: protected WebPage(final PageParameters parameters) {
158: this ((IModel) null);
159: }
160:
161: /**
162: * @see wicket.Component#internalOnAttach()
163: */
164: protected void internalOnAttach() {
165: super .internalOnAttach();
166: // initialize body container
167: getBodyContainer();
168: }
169:
170: /**
171: * Get a facade to the body container for adding onLoad javascript to the
172: * body tag.
173: *
174: * @return The body container
175: */
176: public BodyContainer getBodyContainer() {
177: if (bodyContainer == null) {
178: // Add a Body container if the associated markup contains a <body>
179: // tag
180: // get markup stream gracefully
181: MarkupStream markupStream = getAssociatedMarkupStream(false);
182: if (markupStream != null) {
183: // The default <body> container. It can be accessed, replaced
184: // and attribute modifiers can be attached. <body> tags without
185: // wicket:id get automatically a wicket:id="body" assigned.
186: // find the body tag
187: while (markupStream.hasMore()) {
188: final MarkupElement element = markupStream.next();
189: if (element instanceof ComponentTag) {
190: final ComponentTag tag = (ComponentTag) element;
191: if (tag.isOpen() && TagUtils.isBodyTag(tag)) {
192: // Add a default container if the tag has the
193: // default
194: // name
195: if (BodyOnLoadHandler.BODY_ID.equals(tag
196: .getId())) {
197: add(new HtmlBodyContainer(tag.getId()));
198: }
199: // remember the id of the tag
200: bodyContainer = new BodyContainer(this , tag
201: .getId());
202: break;
203: }
204: }
205: }
206: }
207: }
208:
209: return bodyContainer;
210: }
211:
212: /**
213: * Gets the markup type for a WebPage, which is "html" by default. Support
214: * for pages in another markup language, such as VXML, would require the
215: * creation of a different Page subclass in an appropriate package under
216: * wicket.markup. To support VXML (voice markup), one might create the
217: * package wicket.markup.vxml and a subclass of Page called VoicePage.
218: * <p>
219: * Note: The markup type must be equal to the extension of the markup file.
220: * In the case of WebPages, it must always be "html".
221: *
222: * @return Markup type for HTML
223: */
224: public String getMarkupType() {
225: return "html";
226: }
227:
228: /**
229: * @see wicket.Page#configureResponse()
230: */
231: protected void configureResponse() {
232: super .configureResponse();
233:
234: final WebResponse response = getWebRequestCycle()
235: .getWebResponse();
236: response.setHeader("Pragma", "no-cache");
237: response.setHeader("Cache-Control",
238: "no-cache, max-age=0, must-revalidate"); // no-store
239: }
240:
241: /**
242: * @return The WebRequestCycle for this WebPage.
243: */
244: protected final WebRequestCycle getWebRequestCycle() {
245: return (WebRequestCycle) getRequestCycle();
246: }
247:
248: /**
249: * Creates and returns a bookmarkable link to this application's home page.
250: *
251: * @param id
252: * Name of link
253: * @return Link to home page for this application
254: */
255: protected final BookmarkablePageLink homePageLink(final String id) {
256: return new BookmarkablePageLink(id, getApplication()
257: .getHomePage());
258: }
259:
260: /**
261: * Common code executed by constructors.
262: */
263: private void commonInit() {
264: // if automatic multi window support is on, add a page checker instance
265: if (getApplication().getPageSettings()
266: .getAutomaticMultiWindowSupport()) {
267: add(new PageMapChecker());
268: }
269: }
270:
271: /**
272: * This method is called when the compressing coding and response stategies
273: * are configured in your Application object like this:
274: *
275: * <pre>
276: * protected IRequestCycleProcessor newRequestCycleProcessor()
277: * {
278: * return new CompoundRequestCycleProcessor(new WebURLCompressingCodingStrategy(),
279: * new WebURLCompressingTargetResolverStrategy(), null, null, null);
280: * }
281: * </pre>
282: *
283: * @return The URLCompressor for this webpage.
284: *
285: * @since 1.2
286: *
287: * @see WebURLCompressingCodingStrategy
288: * @see WebURLCompressingTargetResolverStrategy
289: * @see URLCompressor
290: */
291: public final URLCompressor getUrlCompressor() {
292: if (compressor == null) {
293: compressor = new URLCompressor();
294: }
295: return compressor;
296: }
297:
298: /**
299: *
300: * @see wicket.Component#onDetach()
301: */
302: protected void onDetach() {
303: // This code can not go into HtmlHeaderContainer as
304: // header.onEndRequest() is executed inside an iterator
305: // and you can only call container.remove() which
306: // is != iter.remove(). And the iterator is not available
307: // inside onEndRequest(). Obviously WebPage.onEndRequest()
308: // is invoked outside the iterator loop.
309: final Component header = get(HtmlHeaderSectionHandler.HEADER_ID);
310: if (header != null) {
311: this .remove(header);
312: }
313: super .onDetach();
314: }
315:
316: /**
317: * @see wicket.markup.html.INewBrowserWindowListener#onNewBrowserWindow()
318: */
319: public void onNewBrowserWindow() {
320: // if the browser reports a history of 0 then make a new webpage
321: WebPage clonedPage = this ;
322: try {
323: clonedPage = (WebPage) Objects.cloneObject(this );
324: } catch (Exception e) {
325: log.error("Page " + clonedPage
326: + " couldn't be cloned to move to another pagemap",
327: e);
328: }
329: final PageMap map = getSession().createAutoPageMap();
330: clonedPage.moveToPageMap(map);
331: setResponsePage(clonedPage);
332: }
333:
334: /**
335: * Tries to determine whether this page was opened in a new window or tab.
336: * If it is (and this checker were able to recognize that), a new page map
337: * is created for this page instance, so that it will start using it's own
338: * history in sync with the browser window or tab.
339: */
340: private final class PageMapChecker extends AbstractBehavior
341: implements IHeaderContributor {
342: private static final long serialVersionUID = 1L;
343:
344: /** The unload model for deleting the pagemap cookie */
345: private Model onUnLoadModel;
346:
347: /**
348: * @see wicket.markup.html.IHeaderContributor#renderHead(wicket.Response)
349: */
350: public final void renderHead(final Response response) {
351: final WebRequestCycle cycle = (WebRequestCycle) getRequestCycle();
352: final IRequestTarget target = cycle.getRequestTarget();
353:
354: String name = getPageMap().getName();
355: if (name == null) {
356: name = "wicket:default";
357: } else {
358: name = name.replace('"', '_');
359: }
360:
361: Session session = getSession();
362:
363: PageMapAccessMetaData meta = (PageMapAccessMetaData) session
364: .getMetaData(PAGEMAP_ACCESS_MDK);
365: if (meta == null) {
366: meta = new PageMapAccessMetaData();
367: session.setMetaData(PAGEMAP_ACCESS_MDK, meta);
368: }
369: boolean firstAccess = false;
370: if (!meta.pageMapNames.contains(name)) {
371: firstAccess = true;
372: meta.pageMapNames.add(name);
373: }
374:
375: // Here is our trickery to detect whether the current request was
376: // made in a new window/ tab, in which case it should go in a
377: // different page map so that we don't intermangle the history of
378: // those windows
379: CharSequence url = null;
380: if (target instanceof IBookmarkablePageRequestTarget) {
381: IBookmarkablePageRequestTarget current = (IBookmarkablePageRequestTarget) target;
382: BookmarkablePageRequestTarget redirect = new BookmarkablePageRequestTarget(
383: getSession().createAutoPageMapName(), current
384: .getPageClass(), current
385: .getPageParameters());
386: url = cycle.urlFor(redirect);
387: } else {
388: url = urlFor(INewBrowserWindowListener.INTERFACE);
389: }
390: if (firstAccess) {
391: // this is the first access to the pagemap, set window.name
392: JavascriptUtils.writeOpenTag(response);
393: response.write("if (window.name=='') { window.name=\"");
394: response.write(name);
395: response.write("\"; }");
396: JavascriptUtils.writeCloseTag(response);
397: } else {
398: JavascriptUtils.writeOpenTag(response);
399: response
400: .write("if (window.name=='') { window.location=\"");
401: response.write(url);
402: response.write("\"; }");
403: JavascriptUtils.writeCloseTag(response);
404: }
405: }
406: }
407: }
|