001: /**
002: * Copyright 2006 Webmedia Group Ltd.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: **/package org.araneaframework.http.filter;
016:
017: import java.io.PrintWriter;
018: import java.io.Serializable;
019: import java.util.ArrayList;
020: import java.util.HashMap;
021: import java.util.HashSet;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025: import java.util.Set;
026: import org.apache.commons.lang.StringUtils;
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.araneaframework.Component;
030: import org.araneaframework.Environment;
031: import org.araneaframework.Message;
032: import org.araneaframework.OutputData;
033: import org.araneaframework.Path;
034: import org.araneaframework.Widget;
035: import org.araneaframework.core.Assert;
036: import org.araneaframework.core.BroadcastMessage;
037: import org.araneaframework.core.RoutedMessage;
038: import org.araneaframework.core.StandardEnvironment;
039: import org.araneaframework.core.StandardPath;
040: import org.araneaframework.framework.TransactionContext;
041: import org.araneaframework.framework.core.BaseFilterWidget;
042: import org.araneaframework.http.HttpOutputData;
043: import org.araneaframework.http.UpdateRegionContext;
044: import org.araneaframework.http.UpdateRegionProvider;
045: import org.araneaframework.http.util.AtomicResponseHelper;
046: import org.araneaframework.http.util.JsonObject;
047:
048: /**
049: * Update region filter, supporting updating of HTML page regions and sending
050: * miscellaneous data back via AJAX requests.
051: *
052: * @author Nikita Salnikov-Tarnovski
053: * @author "Toomas Römer" <toomas@webmedia.ee>
054: * @author Alar Kvell (alar@araneaframework.org)
055: * @since 1.1
056: */
057: public class StandardUpdateRegionFilterWidget extends BaseFilterWidget
058: implements UpdateRegionContext {
059: static private final Log log = LogFactory
060: .getLog(StandardUpdateRegionFilterWidget.class);
061:
062: private String characterEncoding = "UTF-8";
063: private Map documentRegions = new HashMap();
064: private List renderedRegions = new ArrayList();
065: private boolean disabled = false;
066:
067: public static final String AJAX_REQUEST_ID_KEY = "ajaxRequestId";
068:
069: public static final String RELOAD_REGION_KEY = "reload";
070: public static final String TRANSACTION_ID_REGION_KEY = "transactionId";
071: public static final String DOCUMENT_REGION_KEY = "document";
072:
073: public void setCharacterEncoding(String encoding) {
074: characterEncoding = encoding;
075: }
076:
077: public void disableOnce() {
078: disabled = true;
079: }
080:
081: public void addDocumentRegion(String documentRegionId,
082: String widgetId) {
083: Assert.notEmptyParam(documentRegionId, "regionName");
084: Assert.notEmptyParam(widgetId, "widgetId");
085: documentRegions.put(documentRegionId, widgetId);
086: }
087:
088: public void addRenderedRegion(String documentRegionId) {
089: renderedRegions.add(documentRegionId);
090: }
091:
092: protected Environment getChildWidgetEnvironment() {
093: return new StandardEnvironment(super
094: .getChildWidgetEnvironment(),
095: UpdateRegionContext.class, this );
096: }
097:
098: protected void render(OutputData output) throws Exception {
099: String regionsFromRequest = (String) output.getInputData()
100: .getGlobalData().get(
101: UpdateRegionContext.UPDATE_REGIONS_KEY);
102: StringBuffer regionNames = regionsFromRequest != null ? new StringBuffer(
103: regionsFromRequest)
104: : new StringBuffer();
105:
106: if (!renderedRegions.isEmpty()) {
107: for (Iterator i = renderedRegions.iterator(); i.hasNext();) {
108: regionNames.append(",").append(i.next());
109: }
110: }
111:
112: if (regionNames.length() == 0) {
113: documentRegions.clear();
114: super .render(output);
115: disabled = false;
116: return;
117: }
118:
119: if (log.isDebugEnabled())
120: log.debug("Received request to update regions '"
121: + regionNames + "'");
122:
123: AtomicResponseHelper arUtil = new AtomicResponseHelper(output);
124: try {
125: Map regionContents = null;
126: if (!disabled) {
127: // Parse widget and region ids
128: Map regionIdsByWidgetId = parseRegionNames(regionNames
129: .toString());
130: // Render widgets
131: regionContents = renderRegions(regionIdsByWidgetId,
132: arUtil, output);
133: }
134:
135: // Write out response
136: HttpOutputData httpOutput = (HttpOutputData) output;
137: PrintWriter writer = httpOutput.getWriter();
138: String ajaxRequestId = (String) output.getInputData()
139: .getGlobalData().get(AJAX_REQUEST_ID_KEY);
140: writeResponseId(writer, ajaxRequestId);
141: if (disabled) {
142: if (log.isDebugEnabled())
143: log
144: .debug("Partial rendering is disabled, forcing a reload for full render");
145: writeReloadRegion(writer);
146: } else {
147: writeTransactionIdRegion(writer);
148: writeHandlerRegions(writer);
149: writeDocumentRegions(writer, regionContents);
150: }
151: writer.flush();
152: } finally {
153: arUtil.commit();
154: disabled = false;
155: renderedRegions.clear();
156: }
157: }
158:
159: protected void writeResponseId(PrintWriter out, String responseId)
160: throws Exception {
161: if (responseId != null) {
162: out.write(responseId + "\n");
163: }
164: }
165:
166: protected void writeRegion(PrintWriter out, String name,
167: String content) throws Exception {
168: out.write(name);
169: out.write("\n");
170: out.write(Integer.toString(content.length()));
171: out.write("\n");
172: out.write(content);
173: }
174:
175: protected void writeReloadRegion(PrintWriter out) throws Exception {
176: writeRegion(out, RELOAD_REGION_KEY, "");
177: }
178:
179: protected void writeTransactionIdRegion(PrintWriter out)
180: throws Exception {
181: TransactionContext transactionContext = (TransactionContext) getEnvironment()
182: .getEntry(TransactionContext.class);
183: if (transactionContext != null) {
184: writeRegion(out, TRANSACTION_ID_REGION_KEY,
185: transactionContext.getTransactionId().toString());
186: }
187: }
188:
189: protected void writeHandlerRegions(PrintWriter out)
190: throws Exception {
191: UpdateRegionGatherMessage regionGatherMessage = new UpdateRegionGatherMessage();
192: propagate(regionGatherMessage);
193: for (Iterator i = regionGatherMessage.getRegions().entrySet()
194: .iterator(); i.hasNext();) {
195: Map.Entry entry = (Map.Entry) i.next();
196: String name = (String) entry.getKey();
197: if (log.isDebugEnabled()) {
198: log.debug("Updating handler region : " + name);
199: }
200: String content = (String) entry.getValue();
201: if (content != null) {
202: writeRegion(out, name, content);
203: }
204: }
205: }
206:
207: protected void writeDocumentRegions(PrintWriter out,
208: Map regionContents) throws Exception {
209: for (Iterator i = regionContents.entrySet().iterator(); i
210: .hasNext();) {
211: Map.Entry entry = (Map.Entry) i.next();
212: String id = (String) entry.getKey();
213: Region region = (Region) entry.getValue();
214: JsonObject documentObject = new JsonObject();
215: documentObject.setStringProperty("id", id);
216: documentObject.setStringProperty("mode", region.getMode());
217: StringBuffer buf = new StringBuffer(documentObject
218: .toString());
219: buf.insert(0, buf.length() + "\n");
220: buf.append(region.getContent());
221: writeRegion(out, DOCUMENT_REGION_KEY, buf.toString());
222: }
223: }
224:
225: protected Map parseRegionNames(String commaSeparatedRegionNames) {
226: Map regionIdsByWidgetId = new HashMap();
227:
228: String[] regionNames = StringUtils.split(
229: commaSeparatedRegionNames, ',');
230: for (int i = 0; i < regionNames.length; i++) {
231: String documentRegionId = regionNames[i];
232: String widgetId = (String) documentRegions
233: .get(documentRegionId);
234: if (widgetId == null) {
235: if (log.isWarnEnabled())
236: log.warn("Document region '" + documentRegionId
237: + "' not found");
238: continue;
239: }
240:
241: Set regionIds = (Set) regionIdsByWidgetId.get(widgetId);
242: if (regionIds == null) {
243: regionIds = new HashSet();
244: regionIdsByWidgetId.put(widgetId, regionIds);
245: }
246: regionIds.add(documentRegionId);
247: }
248:
249: removeOverlappingRegions(regionIdsByWidgetId);
250:
251: return regionIdsByWidgetId;
252: }
253:
254: protected void removeOverlappingRegions(Map regionIdsByWidgetId) {
255: String sourceWidgetId = null;
256: Set sourceRegionIds = null;
257: for (Iterator i = regionIdsByWidgetId.entrySet().iterator(); i
258: .hasNext();) {
259: Map.Entry entry = (Map.Entry) i.next();
260:
261: if (sourceRegionIds == null) {
262: sourceWidgetId = (String) entry.getKey();
263: sourceRegionIds = (Set) entry.getValue();
264: continue;
265: }
266:
267: String widgetId = (String) entry.getKey();
268: Set regionIds = (Set) entry.getValue();
269: if (widgetId.startsWith(sourceWidgetId + ".")) {
270: sourceRegionIds.addAll(regionIds);
271: i.remove();
272: } else {
273: sourceWidgetId = widgetId;
274: sourceRegionIds = regionIds;
275: }
276: }
277: }
278:
279: protected Map renderRegions(Map regionIdsByWidgetId,
280: AtomicResponseHelper arUtil, OutputData output)
281: throws Exception {
282: Map regionContents = new HashMap();
283: for (Iterator i = regionIdsByWidgetId.entrySet().iterator(); i
284: .hasNext();) {
285: Map.Entry entry = (Map.Entry) i.next();
286: String widgetId = (String) entry.getKey();
287: Set regionIds = (Set) entry.getValue();
288:
289: if (log.isDebugEnabled())
290: log.debug("Rendering widget '" + widgetId + "'");
291:
292: // send a message to identify the component to be rendered
293: ComponentLocatorMessage componentLocatorMessage = new ComponentLocatorMessage(
294: new StandardPath(widgetId));
295: propagate(componentLocatorMessage);
296: if (componentLocatorMessage.getComponent() == null) {
297: if (log.isWarnEnabled())
298: log.warn("Widget '" + widgetId
299: + "' not found, skipping rendering");
300: continue;
301: }
302:
303: // send a message to renderable component that resets the render state of Renderable components
304: NotRenderedMessage.INSTANCE.send(null,
305: componentLocatorMessage.getComponent());
306:
307: // send a message that renders the identified component
308: Message renderMessage = new RenderMessage(new StandardPath(
309: widgetId), output);
310: propagate(renderMessage);
311:
312: if (disabled) // Our filter was disabled during rendering this widget
313: return null; // force page to reload for full render
314:
315: // Cut out regions by special comments
316: String widgetContent = new String(arUtil.getData(),
317: characterEncoding);
318: for (Iterator j = regionIds.iterator(); j.hasNext();) {
319: String id = (String) j.next();
320: String content = getContentById(widgetContent, id);
321: if (content == null) {
322: if (log.isWarnEnabled())
323: log
324: .warn("Document region '"
325: + id
326: + "' not found on rendering of widget '"
327: + widgetId + "'");
328: continue;
329: }
330: regionContents.put(id, new Region(content, "update"));
331: }
332: arUtil.rollback();
333: }
334: return regionContents;
335: }
336:
337: protected String getContentById(String source, String id) {
338: String blockStart = "<!--BEGIN:" + id + "-->";
339: int startIndex = source.indexOf(blockStart);
340:
341: if (startIndex == -1)
342: return null;
343:
344: String blockEnd = "<!--END:" + id + "-->";
345:
346: int endIndex = source.indexOf(blockEnd);
347:
348: if (endIndex == -1)
349: throw new IllegalStateException(
350: "Expected END block for AJAX update region with id '"
351: + id + "'.");
352:
353: if (log.isDebugEnabled())
354: log.debug("Successfully extracted region '" + id
355: + "' to be included in response.");
356:
357: return source.substring(startIndex + blockStart.length(),
358: endIndex);
359: }
360:
361: public static class Region implements Serializable {
362:
363: private String content;
364: private String mode;
365:
366: public Region(String content, String mode) {
367: this .content = content;
368: this .mode = mode;
369: }
370:
371: public String getContent() {
372: return content;
373: }
374:
375: public String getMode() {
376: return mode;
377: }
378:
379: }
380:
381: public static class ComponentLocatorMessage extends RoutedMessage {
382:
383: private Component component;
384:
385: public ComponentLocatorMessage(Path path) {
386: super (path);
387: }
388:
389: protected void execute(Component component) throws Exception {
390: this .component = component;
391: }
392:
393: public Component getComponent() {
394: return this .component;
395: }
396: }
397:
398: public static class RenderMessage extends RoutedMessage {
399:
400: private OutputData output;
401:
402: public RenderMessage(Path path, OutputData output) {
403: super (path);
404: this .output = output;
405: }
406:
407: protected void execute(Component component) throws Exception {
408: ((Widget) component)._getWidget().render(output);
409: ((HttpOutputData) output).getWriter().flush();
410: }
411:
412: }
413:
414: public static class UpdateRegionGatherMessage extends
415: BroadcastMessage {
416:
417: private Map regions = new HashMap();
418:
419: protected void execute(Component component) throws Exception {
420: if (component instanceof UpdateRegionProvider) {
421: Map newRegions = ((UpdateRegionProvider) component)
422: .getRegions();
423: if (newRegions != null && !newRegions.isEmpty()) {
424: if (log.isWarnEnabled()) {
425: Set duplicateRegions = new HashSet(newRegions
426: .keySet());
427: duplicateRegions.retainAll(regions.keySet());
428: if (duplicateRegions.size() > 0) {
429: log
430: .warn("UpdateRegionProvider '"
431: + (component.getScope() != null ? component
432: .getScope()
433: .toString()
434: : component
435: .getClass()
436: .getName())
437: + "' overwrites previously added regions: "
438: + duplicateRegions
439: .toString());
440: }
441: }
442: regions.putAll(newRegions);
443: }
444: }
445: }
446:
447: public Map getRegions() {
448: return regions;
449: }
450:
451: }
452:
453: }
|