001: /*
002: * This file is part of the Echo Web Application Framework (hereinafter "Echo").
003: * Copyright (C) 2002-2005 NextApp, Inc.
004: *
005: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
006: *
007: * The contents of this file are subject to the Mozilla Public License Version
008: * 1.1 (the "License"); you may not use this file except in compliance with
009: * the License. You may obtain a copy of the License at
010: * http://www.mozilla.org/MPL/
011: *
012: * Software distributed under the License is distributed on an "AS IS" basis,
013: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
014: * for the specific language governing rights and limitations under the
015: * License.
016: *
017: * Alternatively, the contents of this file may be used under the terms of
018: * either the GNU General Public License Version 2 or later (the "GPL"), or
019: * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
020: * in which case the provisions of the GPL or the LGPL are applicable instead
021: * of those above. If you wish to allow use of your version of this file only
022: * under the terms of either the GPL or the LGPL, and not to allow others to
023: * use your version of this file under the terms of the MPL, indicate your
024: * decision by deleting the provisions above and replace them with the notice
025: * and other provisions required by the GPL or the LGPL. If you do not delete
026: * the provisions above, a recipient may use your version of this file under
027: * the terms of any one of the MPL, the GPL or the LGPL.
028: */
029:
030: package nextapp.echo2.webrender;
031:
032: import java.util.HashMap;
033: import java.util.HashSet;
034: import java.util.Map;
035: import java.util.Set;
036:
037: import org.w3c.dom.Document;
038: import org.w3c.dom.Element;
039: import org.w3c.dom.NodeList;
040:
041: import nextapp.echo2.webrender.output.XmlDocument;
042:
043: /**
044: * The outgoing XML message which synchronizes the state of the client to that
045: * of the server.
046: */
047: public class ServerMessage extends XmlDocument {
048:
049: /**
050: * Constant for the use with <code>setRootLayoutDirection()</code>
051: * indicating a left-to-right layout direction.
052: */
053: public static final int LEFT_TO_RIGHT = 0;
054:
055: /**
056: * Constant for the use with <code>setRootLayoutDirection()</code>
057: * indicating a right-to-left layout direction.
058: */
059: public static final int RIGHT_TO_LEFT = 1;
060:
061: /**
062: * A hash table associating <code>ItemizedDirectiveLookupKey</code>s to
063: * corresponding <code>Element</code>s in the message.
064: */
065: private Map itemizedDirectivesMap = new HashMap();
066:
067: /**
068: * Representation of the information required to look up a suitable Itemized
069: * Directive <code>Element</code>. Instances are used as keys in the
070: * <code>itemizedDirectivesMap</code>. This class provides a
071: * <code>getHashCode()</code> implementation for efficient lookups.
072: */
073: private class ItemizedDirectiveLookupKey {
074:
075: String groupId;
076: String processor;
077: String directiveName;
078: String[] keyAttributeNames;
079: String[] keyAttributeValues;
080: int hashCode;
081:
082: /**
083: * Creates an <code>ItemizedDirectiveLookupKey</code> based on the
084: * provided description.
085: *
086: * @param groupId the identifier of the target message part group
087: * @param processor the name of the client-side processor object which
088: * will process the message part containing the directive, e.g.,
089: * "EchoEventUpdate", or "EchoDomUpdate"
090: * @param directiveName the name of the directive, e.g., "event-add" or
091: * "dom-remove".
092: * @param keyAttributeNames
093: * @param keyAttributeValues
094: */
095: private ItemizedDirectiveLookupKey(String groupId,
096: String processor, String directiveName,
097: String[] keyAttributeNames, String[] keyAttributeValues) {
098: super ();
099: this .groupId = groupId;
100: this .processor = processor;
101: this .directiveName = directiveName;
102: this .keyAttributeNames = keyAttributeNames;
103: this .keyAttributeValues = keyAttributeValues;
104: this .hashCode = groupId.hashCode() ^ processor.hashCode()
105: ^ directiveName.hashCode();
106: for (int i = 0; i < keyAttributeNames.length; ++i) {
107: this .hashCode ^= keyAttributeNames[i].hashCode()
108: ^ keyAttributeValues[i].hashCode();
109: }
110: }
111:
112: /**
113: * @see java.lang.Object#equals(java.lang.Object)
114: */
115: public boolean equals(Object o) {
116: if (!(o instanceof ItemizedDirectiveLookupKey)) {
117: return false;
118: }
119: ItemizedDirectiveLookupKey that = (ItemizedDirectiveLookupKey) o;
120:
121: if (!this .groupId.equals(that.groupId)) {
122: return false;
123: }
124: if (!this .processor.equals(that.processor)) {
125: return false;
126: }
127: if (!this .directiveName.equals(that.directiveName)) {
128: return false;
129: }
130: if (this .keyAttributeNames.length != that.keyAttributeNames.length) {
131: return false;
132: }
133: for (int i = 0; i < keyAttributeValues.length; ++i) {
134: if (!(this .keyAttributeValues[i]
135: .equals(that.keyAttributeValues[i]))) {
136: return false;
137: }
138: }
139: for (int i = 0; i < keyAttributeNames.length; ++i) {
140: if (!(this .keyAttributeNames[i]
141: .equals(that.keyAttributeNames[i]))) {
142: return false;
143: }
144: }
145:
146: return true;
147: }
148:
149: /**
150: * @see java.lang.Object#hashCode()
151: */
152: public int hashCode() {
153: return hashCode;
154: }
155: }
156:
157: /**
158: * Constant for the "init" message part group. Message parts in this group are
159: * processed before the "preremove", "remove" "update", and "postupdate"
160: * groups.
161: */
162: public static final String GROUP_ID_INIT = "init";
163:
164: /**
165: * Constant for the "preremove" message part group. Message parts in this group
166: * are processed after the "init" group. Message parts in this group are
167: * processed before the "remove", "update" and "postupdate" groups.
168: */
169: public static final String GROUP_ID_PREREMOVE = "preremove";
170:
171: /**
172: * Constant for the "remove" message part group. Message parts in this group
173: * are processed after the "init" and "preremove" groups. Message parts in
174: * this group are processed before the "update" and "postupdate" groups.
175: * This group is used for removing elements from the DOM.
176: */
177: public static final String GROUP_ID_REMOVE = "remove";
178:
179: /**
180: * Constant for the "update" message part group. Message parts in this group
181: * are processed after the "init", "preremove" and "remove" groups.
182: * Message parts in this group are processed before the "postupdate" group.
183: * This group is used for adding elements to the DOM.
184: */
185: public static final String GROUP_ID_UPDATE = "update";
186:
187: /**
188: * Constant for the "postupdate" message part group. Message parts in this
189: * group are processed after the "init", "preremove", "remove" and "update"
190: * groups.
191: */
192: public static final String GROUP_ID_POSTUPDATE = "postupdate";
193:
194: /** Set of added script libraries. */
195: private Set addedLibraries;
196:
197: /**
198: * DOM <code>libraries</code> Element to which <code>library</code>
199: * elements are added to represent individual dynamically loaded JavaScript
200: * libraries.
201: */
202: private Element librariesElement;
203:
204: /** Root DOM <code>server-message</code> element. */
205: private Element serverMessageElement;
206:
207: /**
208: * Creates a new <code>ServerMessage</code>.
209: */
210: public ServerMessage() {
211: super ("server-message", null, null,
212: "http://www.nextapp.com/products/echo2/svrmsg/servermessage");
213: Document document = getDocument();
214: serverMessageElement = document.getDocumentElement();
215: librariesElement = document.createElement("libraries");
216: serverMessageElement.appendChild(librariesElement);
217:
218: // Add basic part groups.
219: addPartGroup(GROUP_ID_INIT);
220: addPartGroup(GROUP_ID_PREREMOVE);
221: addPartGroup(GROUP_ID_REMOVE);
222: addPartGroup(GROUP_ID_UPDATE);
223: addPartGroup(GROUP_ID_POSTUPDATE);
224: }
225:
226: /**
227: * Adds a JavaScript library service to be dynamically loaded.
228: *
229: * @param serviceId the id of the service to load (the service must return
230: * JavaScript code with content-type "text/javascript")
231: */
232: public void addLibrary(String serviceId) {
233: if (addedLibraries == null) {
234: addedLibraries = new HashSet();
235: }
236: if (addedLibraries.contains(serviceId)) {
237: return;
238: }
239: Element libraryElement = getDocument().createElement("library");
240: libraryElement.setAttribute("service-id", serviceId);
241: librariesElement.appendChild(libraryElement);
242: addedLibraries.add(serviceId);
243: }
244:
245: /**
246: * Adds a "group" to the document. Part groups enable certain groups of
247: * operations, e.g., remove operations, to be performed before others, e.g.,
248: * add operations.
249: *
250: * @param groupId the identifier of the group
251: * @return the created "message-part-group" element.
252: */
253: public Element addPartGroup(String groupId) {
254: Element messagePartGroupElement = getDocument().createElement(
255: "message-part-group");
256: messagePartGroupElement.setAttribute("id", groupId);
257: serverMessageElement.appendChild(messagePartGroupElement);
258: return messagePartGroupElement;
259: }
260:
261: /**
262: * Retrieves the "message-part-group" element pertaining to a specific group.
263: *
264: * @param groupId the id of the group
265: * @return the "message-part-group" element
266: */
267: public Element getPartGroup(String groupId) {
268: NodeList groupList = serverMessageElement
269: .getElementsByTagName("message-part-group");
270: int length = groupList.getLength();
271: for (int i = 0; i < length; ++i) {
272: Element groupElement = (Element) groupList.item(i);
273: if (groupId.equals(groupElement.getAttribute("id"))) {
274: return groupElement;
275: }
276: }
277: return null;
278: }
279:
280: /**
281: * Adds a "message-part" to the document that will be processed by the
282: * specified client-side processor object.
283: *
284: * @param groupId the id of the group to which the "message-part" element
285: * should be added
286: * @param processor the name of the client-side processor object which will
287: * process the message part, e.g., "EchoEventUpdate", or
288: * "EchoDomUpdate"
289: * @return the created "message-part" element
290: */
291: public Element addPart(String groupId, String processor) {
292: Element messagePartGroupElement = getPartGroup(groupId);
293: Element messagePartElement = getDocument().createElement(
294: "message-part");
295: messagePartElement.setAttribute("processor", processor);
296: messagePartGroupElement.appendChild(messagePartElement);
297: return messagePartElement;
298: }
299:
300: /**
301: * Creates and appends a directive element beneath to a message part.
302: * Attempts to append the directive to an existing message part if the last
303: * message part in the specified group happens to have the same processor as
304: * is specified by the <code>processor</code> argument. If this is not
305: * possible, a new "message-part" element is created and the directive is
306: * added to it.
307: *
308: * @param groupId
309: * @param processor the name of the client-side processor object which will
310: * process the message part, e.g., "EchoEventUpdate", or
311: * "EchoDomUpdate"
312: * @param directiveName the name of the directive, e.g., "event-add" or
313: * "dom-remove".
314: * @return the directive element
315: */
316: public Element appendPartDirective(String groupId,
317: String processor, String directiveName) {
318: Element messagePartElement = null;
319: Element groupElement = getPartGroup(groupId);
320:
321: Element lastChild = (Element) groupElement.getLastChild();
322: if (lastChild != null
323: && processor
324: .equals(lastChild.getAttribute("processor"))) {
325: messagePartElement = lastChild;
326: } else {
327: messagePartElement = addPart(groupId, processor);
328: }
329:
330: Element directiveElement = getDocument().createElement(
331: directiveName);
332: messagePartElement.appendChild(directiveElement);
333: return directiveElement;
334: }
335:
336: /**
337: * Creates or retrieves a suitable "Itemized Directive" element. Itemized
338: * Directives may be used to create more bandwidth-efficient ServerMessage
339: * output in cases where a particular operation may need to be performed on
340: * a significant number of targets. In the case that the directive must be
341: * created, it will be added to the message. Repeated invocations of this
342: * method with equivalent values of all parameters will result in the same
343: * directive being returned each time.
344: *
345: * This method should only be used for adding directives to the
346: * <code>GROUP_ID_PREREMOVE</code> and <code>GROUP_ID_POSTUPDATE</code>
347: * groups as itemized directives will not be executed in-order, which
348: * will cause problems if they are used for to manipulate the DOM.
349: *
350: * @param groupId the identifier of the target message part group,
351: * either <code>GROUP_ID_PREREMOVE</code> or
352: * <code>GROUP_ID_POSTUPDATE</code>
353: * @param processor the name of the client-side processor object which will
354: * process the message part containing the directive, e.g.,
355: * "EchoEventUpdate", or "EchoDomUpdate"
356: * @param directiveName the name of the directive, e.g., "event-add" or
357: * "dom-remove"
358: * @param keyAttributeNames the names of the key attributes
359: * @param keyAttributeValues the values of the key attributes
360: * @return the created/retrieved directive element
361: */
362: public Element getItemizedDirective(String groupId,
363: String processor, String directiveName,
364: String[] keyAttributeNames, String[] keyAttributeValues) {
365: ItemizedDirectiveLookupKey itemizedDirectiveLookupKey = new ItemizedDirectiveLookupKey(
366: groupId, processor, directiveName, keyAttributeNames,
367: keyAttributeValues);
368: Element element = (Element) itemizedDirectivesMap
369: .get(itemizedDirectiveLookupKey);
370: if (element == null) {
371: element = appendPartDirective(groupId, processor,
372: directiveName);
373: for (int i = 0; i < keyAttributeNames.length; ++i) {
374: element.setAttribute(keyAttributeNames[i],
375: keyAttributeValues[i]);
376: }
377: itemizedDirectivesMap.put(itemizedDirectiveLookupKey,
378: element);
379: }
380: return element;
381: }
382:
383: /**
384: * Sets the interval between asynchronous requests to the server to check
385: * for server-pushed updates.
386: *
387: * @param newValue the new interval in milliseconds (a negative value will
388: * disable asynchronous requests)
389: */
390: public void setAsynchronousMonitorInterval(int newValue) {
391: if (newValue < 0) {
392: serverMessageElement.setAttribute("async-interval",
393: "disable");
394: } else {
395: serverMessageElement.setAttribute("async-interval", Integer
396: .toString(newValue));
397: }
398: }
399:
400: /**
401: * Sets the element id of the root of the modal context. Only elements
402: * within the modal context will be enabled. A <code>id</code> value of
403: * null will disable the modal context, thus allowing ALL elements to be
404: * enabled.
405: *
406: * @param id the root element id of the modal context
407: */
408: public void setModalContextRootId(String id) {
409: if (id == null) {
410: serverMessageElement.setAttribute("modal-id", "");
411: } else {
412: serverMessageElement.setAttribute("modal-id", id);
413: }
414: }
415:
416: /**
417: * Sets the root layout direction of the application, i.e., either
418: * <code>LEFT_TO_RIGHT</code> or <code>RIGHT_TO_LEFT</code>.
419: *
420: * @param layoutDirection the new layout direction
421: */
422: public void setRootLayoutDirection(int layoutDirection) {
423: switch (layoutDirection) {
424: case LEFT_TO_RIGHT:
425: serverMessageElement.setAttribute("root-layout-direction",
426: "ltr");
427: break;
428: case RIGHT_TO_LEFT:
429: serverMessageElement.setAttribute("root-layout-direction",
430: "rtl");
431: break;
432: default:
433: throw new IllegalArgumentException(
434: "Illegal layout direction.");
435: }
436: }
437:
438: /**
439: * Sets the numeric identifier for this transaction, which will be returned
440: * in next client message.
441: *
442: * @param transactionId the transaction identifier
443: */
444: public void setTransactionId(long transactionId) {
445: serverMessageElement.setAttribute("trans-id", Long
446: .toString(transactionId));
447: }
448: }
|