001 // Copyright May 8, 2006 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014 package org.apache.tapestry.services.impl;
015
016 import java.io.IOException;
017 import java.io.PrintWriter;
018 import java.util.ArrayList;
019 import java.util.HashMap;
020 import java.util.Iterator;
021 import java.util.List;
022 import java.util.Map;
023
024 import org.apache.hivemind.util.Defense;
025 import org.apache.tapestry.IComponent;
026 import org.apache.tapestry.IForm;
027 import org.apache.tapestry.IMarkupWriter;
028 import org.apache.tapestry.IPage;
029 import org.apache.tapestry.IRender;
030 import org.apache.tapestry.IRequestCycle;
031 import org.apache.tapestry.NestedMarkupWriter;
032 import org.apache.tapestry.engine.NullWriter;
033 import org.apache.tapestry.markup.MarkupWriterSource;
034 import org.apache.tapestry.markup.NestedMarkupWriterImpl;
035 import org.apache.tapestry.services.RequestLocaleManager;
036 import org.apache.tapestry.services.ResponseBuilder;
037 import org.apache.tapestry.services.ServiceConstants;
038 import org.apache.tapestry.util.ContentType;
039 import org.apache.tapestry.util.ScriptUtils;
040 import org.apache.tapestry.web.WebResponse;
041
042
043 /**
044 * Main class that handles dojo based ajax responses. These responses are wrapped
045 * by an xml document format that segments off invididual component/javascript response
046 * types into easy to manage xml elements that can then be interpreted and managed by
047 * running client-side javascript.
048 *
049 * @author jkuhnert
050 */
051 public class DojoAjaxResponseBuilder implements ResponseBuilder
052 {
053 // used to create IMarkupWriter
054 private RequestLocaleManager _localeManager;
055 private MarkupWriterSource _markupWriterSource;
056 private WebResponse _webResponse;
057 private List _errorPages;
058
059 // our response writer
060 private IMarkupWriter _writer;
061 // Parts that will be updated.
062 private List _parts = new ArrayList();
063 // Map of specialized writers, like scripts
064 private Map _writers = new HashMap();
065
066 private IRequestCycle _cycle;
067
068 /**
069 * Creates a builder with a pre-configured {@link IMarkupWriter}.
070 * Currently only used for testing.
071 *
072 * @param writer
073 * The markup writer to render all "good" content to.
074 * @param parts
075 * A set of string ids of the components that may have
076 * their responses rendered.
077 */
078 public DojoAjaxResponseBuilder(IRequestCycle cycle, IMarkupWriter writer, List parts)
079 {
080 Defense.notNull(cycle, "cycle");
081 Defense.notNull(writer, "writer");
082
083 _writer = writer;
084 _cycle = cycle;
085
086 if (parts != null)
087 _parts.addAll(parts);
088 }
089
090 /**
091 * Creates a new response builder with the required services it needs
092 * to render the response when {@link #renderResponse(IRequestCycle)} is called.
093 *
094 * @param localeManager
095 * Used to set the locale on the response.
096 * @param markupWriterSource
097 * Creates IJSONWriter instance to be used.
098 * @param webResponse
099 * Web response for output stream.
100 */
101 public DojoAjaxResponseBuilder(IRequestCycle cycle,
102 RequestLocaleManager localeManager,
103 MarkupWriterSource markupWriterSource,
104 WebResponse webResponse, List errorPages)
105 {
106 Defense.notNull(cycle, "cycle");
107
108 _cycle = cycle;
109 _localeManager = localeManager;
110 _markupWriterSource = markupWriterSource;
111 _webResponse = webResponse;
112 _errorPages = errorPages;
113 }
114
115 /**
116 *
117 * {@inheritDoc}
118 */
119 public boolean isDynamic()
120 {
121 return Boolean.TRUE;
122 }
123
124 /**
125 * {@inheritDoc}
126 */
127 public void renderResponse(IRequestCycle cycle)
128 throws IOException
129 {
130 _localeManager.persistLocale();
131
132 ContentType contentType = new ContentType(CONTENT_TYPE
133 + ";charset=" + cycle.getInfrastructure().getOutputEncoding());
134
135 String encoding = contentType.getParameter(ENCODING_KEY);
136
137 if (encoding == null)
138 {
139 encoding = cycle.getEngine().getOutputEncoding();
140
141 contentType.setParameter(ENCODING_KEY, encoding);
142 }
143
144 PrintWriter printWriter = _webResponse.getPrintWriter(contentType);
145
146 _writer = _markupWriterSource.newMarkupWriter(printWriter, contentType);
147
148 parseParameters(cycle);
149
150 beginResponse();
151
152 // render response
153 cycle.renderPage(this);
154
155 endResponse();
156
157 _writer.close();
158 }
159
160 /**
161 * {@inheritDoc}
162 */
163 public void updateComponent(String id)
164 {
165 if (!_parts.contains(id))
166 _parts.add(id);
167 }
168
169 /**
170 * {@inheritDoc}
171 */
172 public IMarkupWriter getWriter()
173 {
174 return _writer;
175 }
176
177 /**
178 * {@inheritDoc}
179 */
180 public boolean isBodyScriptAllowed(IComponent target)
181 {
182 if (target != null
183 && IForm.class.isInstance(target)
184 && ((IForm)target).isFormFieldUpdating())
185 return true;
186
187 return contains(target);
188 }
189
190 /**
191 * {@inheritDoc}
192 */
193 public boolean isExternalScriptAllowed(IComponent target)
194 {
195 if (target != null
196 && IForm.class.isInstance(target)
197 && ((IForm)target).isFormFieldUpdating())
198 return true;
199
200 return contains(target);
201 }
202
203 /**
204 * {@inheritDoc}
205 */
206 public boolean isInitializationScriptAllowed(IComponent target)
207 {
208 if (target != null
209 && IForm.class.isInstance(target)
210 && ((IForm)target).isFormFieldUpdating())
211 return true;
212
213 return contains(target);
214 }
215
216 /**
217 * {@inheritDoc}
218 */
219 public boolean isImageInitializationAllowed(IComponent target)
220 {
221 if (target != null
222 && IForm.class.isInstance(target)
223 && ((IForm)target).isFormFieldUpdating())
224 return true;
225
226 return contains(target);
227 }
228
229 /**
230 * {@inheritDoc}
231 */
232 public void beginBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
233 {
234 IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
235
236 writer.begin("script");
237 writer.printRaw("\n//<![CDATA[\n");
238 }
239
240 /**
241 * {@inheritDoc}
242 */
243 public void endBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
244 {
245 IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
246
247 writer.printRaw("\n//]]>\n");
248 writer.end();
249 }
250
251 /**
252 * {@inheritDoc}
253 */
254 public void writeBodyScript(IMarkupWriter normalWriter, String script, IRequestCycle cycle)
255 {
256 IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
257
258 writer.printRaw(script);
259 }
260
261 /**
262 * {@inheritDoc}
263 */
264 public void writeExternalScript(IMarkupWriter normalWriter, String url, IRequestCycle cycle)
265 {
266 IMarkupWriter writer = getWriter(ResponseBuilder.INCLUDE_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
267
268 // causes asset includes to be loaded dynamically into document head
269 writer.printRaw("tapestry.loadScriptFromUrl(\"");
270 writer.print(url);
271 writer.printRaw("\");");
272
273 writer.println();
274 }
275
276 /**
277 * {@inheritDoc}
278 */
279 public void writeImageInitializations(IMarkupWriter normalWriter, String script, String preloadName, IRequestCycle cycle)
280 {
281 IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
282
283 writer.printRaw("\n\nvar " + preloadName + " = new Array();\n");
284 writer.printRaw("if (document.images)\n");
285 writer.printRaw("{\n");
286
287 writer.printRaw(script);
288
289 writer.printRaw("}\n");
290 }
291
292 /**
293 * {@inheritDoc}
294 */
295 public void writeInitializationScript(IMarkupWriter normalWriter, String script)
296 {
297 IMarkupWriter writer = getWriter(ResponseBuilder.INITIALIZATION_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
298
299 writer.begin("script");
300
301 // return is in XML so must escape any potentially non-xml compliant content
302 writer.printRaw("\n//<![CDATA[\n");
303
304 writer.printRaw(script);
305
306 writer.printRaw("\n//]]>\n");
307
308 writer.end();
309 }
310
311 /**
312 * {@inheritDoc}
313 */
314 public void render(IMarkupWriter writer, IRender render, IRequestCycle cycle)
315 {
316 // must be a valid writer already
317 if (NestedMarkupWriterImpl.class.isInstance(writer)) {
318 render.render(writer, cycle);
319 return;
320 }
321
322 // check for page exception renders and write content to writer so client can display them
323 if (IPage.class.isInstance(render)) {
324 String errorPage = getErrorPage(((IPage)render).getPageName());
325 if (errorPage != null) {
326 render.render(getWriter(errorPage, EXCEPTION_TYPE), cycle);
327 return;
328 }
329 }
330
331 if (IComponent.class.isInstance(render)
332 && contains((IComponent)render))
333 {
334 render.render(getComponentWriter((IComponent)render), cycle);
335 return;
336 }
337
338 // Nothing else found, throw out response
339 render.render(NullWriter.getSharedInstance(), cycle);
340 }
341
342 private String getErrorPage(String pageName)
343 {
344 for (int i=0; i < _errorPages.size(); i++) {
345 String page = (String)_errorPages.get(i);
346
347 if (pageName.indexOf(page) > -1)
348 return page;
349 }
350
351 return null;
352 }
353
354 /**
355 * Gets a {@link NestedMarkupWriter} for the specified
356 * component to write to and caches the buffer for later
357 * writing.
358 *
359 * @param target
360 * The component to get a writer for.
361 * @return An IMarkuPWriter instance.
362 */
363 IMarkupWriter getComponentWriter(IComponent target)
364 {
365 String id = getComponentId(target);
366
367 return getWriter(id, ELEMENT_TYPE);
368 }
369
370 /**
371 *
372 * {@inheritDoc}
373 */
374 public IMarkupWriter getWriter(String id, String type)
375 {
376 Defense.notNull(id, "id can't be null");
377
378 IMarkupWriter w = (IMarkupWriter)_writers.get(id);
379 if (w != null)
380 return w;
381
382 // Make component write to a "nested" writer
383 // so that element begin/ends don't conflict
384 // with xml element response begin/ends. This is very
385 // important.
386
387 IMarkupWriter nestedWriter = _writer.getNestedWriter();
388 nestedWriter.begin("response");
389 nestedWriter.attribute("id", id);
390 if (type != null)
391 nestedWriter.attribute("type", type);
392
393 _writers.put(id, nestedWriter);
394
395 return nestedWriter;
396 }
397
398 /**
399 * Called to start an ajax response. Writes xml doctype and starts
400 * the <code>ajax-response</code> element that will contain all of
401 * the returned content.
402 */
403 void beginResponse()
404 {
405 _writer.printRaw("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
406 _writer.printRaw("<!DOCTYPE html "
407 + "PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
408 + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\" [\n"
409 + "<!ENTITY nbsp ' '>\n"
410 + "]>\n");
411 _writer.printRaw("<ajax-response>");
412 }
413
414 /**
415 * Called after the entire response has been captured. Causes
416 * the writer buffer output captured to be segmented and written
417 * out to the right response elements for the client libraries to parse.
418 */
419 void endResponse()
420 {
421 // write out captured content
422 Iterator keys = _writers.keySet().iterator();
423 while (keys.hasNext()) {
424
425 String key = (String)keys.next();
426 NestedMarkupWriter nw = (NestedMarkupWriter)_writers.get(key);
427
428 nw.end();
429
430 if (!isScriptWriter(key))
431 _writer.printRaw(ScriptUtils.ensureValidScriptTags(nw.getBuffer()));
432 else
433 _writer.printRaw(nw.getBuffer());
434 }
435
436 //end response
437 _writer.printRaw("</ajax-response>");
438 _writer.flush();
439 }
440
441 /**
442 * Determines if the specified markup writer key is one of
443 * the pre-defined script keys from ResponseBuilder.
444 *
445 * @param key
446 * The key to check.
447 * @return True, if key is one of the ResponseBuilder keys.
448 * (BODY_SCRIPT,INCLUDE_SCRIPT,INITIALIZATION_SCRIPT)
449 */
450 boolean isScriptWriter(String key)
451 {
452 if (key == null)
453 return false;
454
455 if (ResponseBuilder.BODY_SCRIPT.equals(key)
456 || ResponseBuilder.INCLUDE_SCRIPT.equals(key)
457 || ResponseBuilder.INITIALIZATION_SCRIPT.equals(key))
458 return true;
459
460 return false;
461 }
462
463 /**
464 * Grabs the incoming parameters needed for json responses, most notable the
465 * {@link ServiceConstants#UPDATE_PARTS} parameter.
466 *
467 * @param cycle
468 * The request cycle to parse from
469 */
470 void parseParameters(IRequestCycle cycle)
471 {
472 Object[] updateParts = cycle.getParameters(ServiceConstants.UPDATE_PARTS);
473
474 if (updateParts == null)
475 return;
476
477 for(int i = 0; i < updateParts.length; i++)
478 _parts.add(updateParts[i].toString());
479 }
480
481 /**
482 * Determines if the specified component is contained in the
483 * responses requested update parts.
484 * @param target
485 * The component to check for.
486 * @return True if the request should capture the components output.
487 */
488 public boolean contains(IComponent target)
489 {
490 if (target == null)
491 return false;
492
493 String id = getComponentId(target);
494
495 if (_parts.contains(id))
496 return true;
497
498 Iterator it = _cycle.renderStackIterator();
499 while (it.hasNext()) {
500
501 IComponent comp = (IComponent)it.next();
502 String compId = getComponentId(comp);
503
504 if (comp != target && _parts.contains(compId))
505 return true;
506 }
507
508 return false;
509 }
510
511 /**
512 * Gets the id of the specified component, choosing the "id" element
513 * binding over any other id.
514 * @param comp
515 * @return The id of the component.
516 */
517 String getComponentId(IComponent comp)
518 {
519 return comp.getClientId();
520 }
521 }