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.app.update;
031:
032: import java.io.Serializable;
033: import java.util.ArrayList;
034: import java.util.Arrays;
035: import java.util.Collection;
036: import java.util.Comparator;
037: import java.util.HashMap;
038: import java.util.Iterator;
039: import java.util.Map;
040:
041: import nextapp.echo2.app.ApplicationInstance;
042: import nextapp.echo2.app.Command;
043: import nextapp.echo2.app.Component;
044:
045: /**
046: * Monitors updates to component hierarchy and records deltas between
047: * server state of application and client state of application.
048: */
049: public class ServerUpdateManager implements Serializable {
050:
051: /**
052: * <code>Comparator</code> to sort components by their depth in the
053: * hierarchy.
054: */
055: private static final Comparator hierarchyDepthUpdateComparator = new Comparator() {
056:
057: /**
058: * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
059: */
060: public int compare(Object a, Object b) {
061: return getDepth(((ServerComponentUpdate) a).getParent())
062: - getDepth(((ServerComponentUpdate) b).getParent());
063: }
064:
065: /**
066: * @see java.lang.Object#equals(java.lang.Object)
067: */
068: public boolean equals(Object o) {
069: return false;
070: }
071:
072: /**
073: * Returns the depth of the specified component in the hierarchy.
074: *
075: * @param component the component
076: * @return the depth
077: */
078: private int getDepth(Component component) {
079: int count = 0;
080: while (component != null) {
081: component = component.getParent();
082: ++count;
083: }
084: return count;
085: }
086: };
087:
088: private Map applicationUpdateMap;
089: private ArrayList commands;
090: private Map componentUpdateMap;
091: private ServerComponentUpdate fullRefreshUpdate;
092: private ClientUpdateManager clientUpdateManager;
093: private ApplicationInstance applicationInstance;
094:
095: /**
096: * Creates a new <code>ServerUpdateManager</code>.
097: *
098: * <strong>Warning:</strong> the <code>init()</code> method must be
099: * invoked before the <code>ServerUpdateManager</code> is used.
100: *
101: * @param applicationInstance the relevant <code>ApplicationInstance</code>
102: * @see #init(nextapp.echo2.app.update.ClientUpdateManager)
103: */
104: public ServerUpdateManager(ApplicationInstance applicationInstance) {
105: super ();
106: this .applicationInstance = applicationInstance;
107: applicationUpdateMap = new HashMap();
108: commands = new ArrayList();
109: componentUpdateMap = new HashMap();
110: fullRefreshUpdate = new ServerComponentUpdate(null);
111: }
112:
113: /**
114: * Creates or retrieves a <code>ComponentUpdate</code> for the given
115: * parent component. If a <code>ComponentUpdate</code> is created, it
116: * is automatically stored in the update queue.
117: *
118: * @param parent the parent component of the update, i.e., the component
119: * which has had children added, removed, or its properties updated
120: * @return the created or retrieved update
121: */
122: private ServerComponentUpdate createComponentUpdate(Component parent) {
123:
124: ServerComponentUpdate update;
125: if (componentUpdateMap.containsKey(parent)) {
126: update = (ServerComponentUpdate) componentUpdateMap
127: .get(parent);
128: } else {
129: update = new ServerComponentUpdate(parent);
130: componentUpdateMap.put(parent, update);
131: }
132: return update;
133: }
134:
135: /**
136: * Enqueues a <code>Command</code> for processing.
137: *
138: * @param command the command
139: */
140: public void enqueueCommand(Command command) {
141: commands.add(command);
142: }
143:
144: /**
145: * Returns a <code>PropertyUpdate</code> representing the
146: * application-level property update with the specified name.
147: * If the specified property has not been updated, null is returned.
148: *
149: * @param propertyName the name of the property
150: * @return the <code>PropertyUpdate</code>
151: */
152: public PropertyUpdate getApplicationPropertyUpdate(
153: String propertyName) {
154: return (PropertyUpdate) applicationUpdateMap.get(propertyName);
155: }
156:
157: /**
158: * Returns the stored <code>Command</code>s. The commands
159: * are NOT removed or modified by this call.
160: *
161: * @return the commands
162: */
163: public Command[] getCommands() {
164: return (Command[]) commands
165: .toArray(new Command[commands.size()]);
166: }
167:
168: /**
169: * Returns the stored <code>ServerComponentUpdate</code>s. The updates
170: * are NOT removed or modified by this call. The updates will be returned
171: * sorted by depth of their parent components within the hierarchy, but in
172: * otherwise random order.
173: *
174: * @return the updates
175: */
176: public ServerComponentUpdate[] getComponentUpdates() {
177: if (isFullRefreshRequired()) {
178: return new ServerComponentUpdate[] { fullRefreshUpdate };
179: } else {
180: Collection hierarchyUpdates = componentUpdateMap.values();
181: ServerComponentUpdate[] serverComponentUpdates = (ServerComponentUpdate[]) hierarchyUpdates
182: .toArray(new ServerComponentUpdate[hierarchyUpdates
183: .size()]);
184: Arrays.sort(serverComponentUpdates,
185: hierarchyDepthUpdateComparator);
186: return serverComponentUpdates;
187: }
188: }
189:
190: /**
191: * Initialization life-cycle method. Must be invoked before using
192: * the <code>ServerUpdateManager</code>.
193: *
194: * @param clientUpdateManager the <code>ClientUpdateManager</code> that
195: * will be used to process input from the client
196: */
197: public void init(ClientUpdateManager clientUpdateManager) {
198: this .clientUpdateManager = clientUpdateManager;
199: }
200:
201: /**
202: * Determines if an ancestor of the given component is being added.
203: *
204: * @param component the <code>Component</code> to investigate
205: * @return true if an ancestor of the component is being added
206: */
207: private boolean isAncestorBeingAdded(Component component) {
208: Component child = component;
209: Component parent = component.getParent();
210: while (parent != null) {
211: ServerComponentUpdate update = (ServerComponentUpdate) componentUpdateMap
212: .get(parent);
213: if (update != null) {
214: if (update.hasAddedChild(child)) {
215: return true;
216: }
217: }
218: child = parent;
219: parent = parent.getParent();
220: }
221: return false;
222: }
223:
224: /**
225: * Determines if the manager has no updates.
226: *
227: * @return true if the manager has no updates
228: */
229: public boolean isEmpty() {
230: return componentUpdateMap.size() == 0;
231: }
232:
233: /**
234: * Determines if a full refresh of the client state is required.
235: *
236: * @return true if a full refresh is required
237: */
238: public boolean isFullRefreshRequired() {
239: return fullRefreshUpdate != null;
240: }
241:
242: /**
243: * Processes an update to a property of the <code>ApplicationInstance</code>.
244: *
245: * @param propertyName the name of the property
246: * @param oldValue the previous value of the property
247: * @param newValue the current value of the property
248: */
249: public void processApplicationPropertyUpdate(String propertyName,
250: Object oldValue, Object newValue) {
251: Object clientValue = clientUpdateManager
252: .getApplicationUpdatePropertyValue(propertyName);
253: if (clientValue == newValue
254: || (clientValue != null && clientValue.equals(newValue))) {
255: // New value is same as client value, thus client is already in sync: cancel the update.
256: applicationUpdateMap.remove(propertyName);
257: } else {
258: applicationUpdateMap.put(propertyName, new PropertyUpdate(
259: oldValue, newValue));
260: }
261: }
262:
263: /**
264: * Processes the addition of a component to the hierarchy.
265: * Creates/updates a <code>ServerComponentUpdate</code> if required.
266: *
267: * @param parent a component which currently exists in the hierarchy
268: * @param child the component which was added to <code>parent</code>
269: */
270: public void processComponentAdd(Component parent, Component child) {
271: if (isFullRefreshRequired()) {
272: return;
273: }
274: if (!child.isRenderVisible()) {
275: return;
276: }
277: if (isAncestorBeingAdded(child)) {
278: return;
279: }
280:
281: ServerComponentUpdate update = createComponentUpdate(parent);
282: update.addChild(child);
283: }
284:
285: /**
286: * Processes an update to the <code>LayoutData</code> of a component.
287: * Creates/updates a <code>ServerComponentUpdate</code> if required.
288: *
289: * @param updatedComponent a component which currently exists in the
290: * hierarchy whose <code>LayoutData</code> has changed
291: */
292: public void processComponentLayoutDataUpdate(
293: Component updatedComponent) {
294: if (isFullRefreshRequired()) {
295: return;
296: }
297: if (!updatedComponent.isRenderVisible()) {
298: return;
299: }
300:
301: Component parentComponent = updatedComponent.getParent();
302: if (parentComponent == null
303: || isAncestorBeingAdded(parentComponent)) {
304: // Do nothing.
305: return;
306: }
307: ServerComponentUpdate update = createComponentUpdate(parentComponent);
308: update.updateLayoutData(updatedComponent);
309: }
310:
311: /**
312: * Processes an update to a property of a component (other than the
313: * <code>LayoutData</code> property).
314: * Creates/updates a <code>ServerComponentUpdate</code> if required.
315: *
316: * @param updatedComponent the component whose property(s) changed.
317: * @param propertyName the name of the changed property
318: * @param oldValue The previous value of the property
319: * @param newValue The new value of the property
320: */
321: public void processComponentPropertyUpdate(
322: Component updatedComponent, String propertyName,
323: Object oldValue, Object newValue) {
324: if (isFullRefreshRequired()) {
325: return;
326: }
327: if (!updatedComponent.isRenderVisible()) {
328: return;
329: }
330: if (isAncestorBeingAdded(updatedComponent)) {
331: return;
332: }
333:
334: // Do not add update (and if necessary cancel any update) if the property is being updated
335: // as the result of input from the client (and thus client and server state of property are
336: // already synchronized).
337: ClientComponentUpdate clientComponentUpdate = clientUpdateManager
338: .getComponentUpdate(updatedComponent);
339: if (clientComponentUpdate != null) {
340: if (clientComponentUpdate.hasInput(propertyName)) {
341: Object inputValue = clientComponentUpdate
342: .getInputValue(propertyName);
343: if (inputValue == newValue
344: || (inputValue != null && inputValue
345: .equals(newValue))) {
346: ServerComponentUpdate update = (ServerComponentUpdate) componentUpdateMap
347: .get(updatedComponent);
348: if (update != null) {
349: update.cancelUpdateProperty(propertyName);
350: }
351: return;
352: }
353: }
354: }
355:
356: ServerComponentUpdate update = createComponentUpdate(updatedComponent);
357: update.updateProperty(propertyName, oldValue, newValue);
358: }
359:
360: /**
361: * Processes the removal of a component from the hierarchy.
362: * Creates/updates a <code>ServerComponentUpdate</code> if required.
363: *
364: * @param parent a component which currently exists in the hierarchy
365: * @param child the component which was removed from <code>parent</code>
366: */
367: public void processComponentRemove(Component parent, Component child) {
368: if (isFullRefreshRequired()) {
369: return;
370: }
371: if (!parent.isRenderVisible()) {
372: return;
373: }
374: if (isAncestorBeingAdded(parent)) {
375: return;
376: }
377: ServerComponentUpdate update = createComponentUpdate(parent);
378: update.removeChild(child);
379:
380: // Search updated components for descendants of removed component.
381: // Any found descendants will be removed and added to this update's
382: // list of removed descendants.
383: Iterator it = componentUpdateMap.keySet().iterator();
384: while (it.hasNext()) {
385: Component testComponent = (Component) it.next();
386: if (child.isAncestorOf(testComponent)) {
387: ServerComponentUpdate childUpdate = (ServerComponentUpdate) componentUpdateMap
388: .get(testComponent);
389: update.appendRemovedDescendants(childUpdate);
390: it.remove();
391: }
392: }
393: }
394:
395: /**
396: * Processes an update to the visible state of a component.
397: * Creates/updates a <code>ServerComponentUpdate</code> if required.
398: *
399: * @param updatedComponent a component which currently exists in the
400: * hierarchy whose visible state has changed.
401: */
402: public void processComponentVisibilityUpdate(
403: Component updatedComponent) {
404: Component parentComponent = updatedComponent.getParent();
405: if (updatedComponent.isVisible()) {
406: processComponentAdd(parentComponent, updatedComponent);
407: } else {
408: processComponentRemove(parentComponent, updatedComponent);
409: }
410: }
411:
412: /**
413: * Processes a full refresh of the application state, in response to a
414: * severe change, such as application locale or style sheet.
415: */
416: public void processFullRefresh() {
417: if (fullRefreshUpdate != null) {
418: return;
419: }
420:
421: fullRefreshUpdate = new ServerComponentUpdate(null);
422:
423: if (applicationInstance.getDefaultWindow() != null) {
424: // Default window may be null if an operation is invoked from within the
425: // ApplicationInstsance.init() implementation that causes a full refresh.
426: fullRefreshUpdate.removeDescendant(applicationInstance
427: .getDefaultWindow());
428: }
429:
430: Iterator it = componentUpdateMap.keySet().iterator();
431: while (it.hasNext()) {
432: Component testComponent = (Component) it.next();
433: ServerComponentUpdate childUpdate = (ServerComponentUpdate) componentUpdateMap
434: .get(testComponent);
435: fullRefreshUpdate.appendRemovedDescendants(childUpdate);
436: it.remove();
437: }
438: }
439:
440: /**
441: * Removes all <code>ServerComponentUpdate</code>s from the manager,
442: * resetting its state to zero. This method is invoked by the
443: * container once it has retrieved and processed all available updates.
444: */
445: void purge() {
446: applicationUpdateMap.clear();
447: componentUpdateMap.clear();
448: commands.clear();
449: fullRefreshUpdate = null;
450: }
451: }
|