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.service;
031:
032: import java.io.ByteArrayInputStream;
033: import java.io.ByteArrayOutputStream;
034: import java.io.IOException;
035: import java.io.InputStream;
036: import java.util.HashMap;
037: import java.util.Map;
038:
039: import javax.servlet.http.HttpServletRequest;
040:
041: import org.w3c.dom.Document;
042: import org.w3c.dom.Element;
043: import org.xml.sax.SAXException;
044:
045: import nextapp.echo2.webrender.ClientAnalyzerProcessor;
046: import nextapp.echo2.webrender.Connection;
047: import nextapp.echo2.webrender.ContentType;
048: import nextapp.echo2.webrender.ServerMessage;
049: import nextapp.echo2.webrender.Service;
050: import nextapp.echo2.webrender.UserInstance;
051: import nextapp.echo2.webrender.UserInstanceUpdateManager;
052: import nextapp.echo2.webrender.servermessage.ClientConfigurationUpdate;
053: import nextapp.echo2.webrender.servermessage.ClientPropertiesStore;
054: import nextapp.echo2.webrender.servermessage.ServerDelayMessageUpdate;
055: import nextapp.echo2.webrender.util.DomUtil;
056:
057: /**
058: * A service which synchronizes the state of the client with that of the server.
059: * Requests made to this service are in the form of "ClientMessage" XML
060: * documents which describe the user's actions since the last synchronization,
061: * e.g., the input typed into text fields and the action taken (e.g., a button
062: * press) which caused the server interaction. The service parses this XML input
063: * from the client and performs updates to the server state of the application.
064: * Once the input has been processed by the server application, an output
065: * "ServerMessage" containing instructions to update the client state is
066: * generated as a response.
067: */
068: public abstract class SynchronizeService implements Service {
069:
070: /**
071: * An interface describing a ClientMessage MessagePart Processor.
072: * Implementations registered with the
073: * <code>registerClientMessagePartProcessor()</code> method will have
074: * their <code>process()</code> methods invoked when a matching
075: * message part is provided in a ClientMessage.
076: */
077: public static interface ClientMessagePartProcessor {
078:
079: /**
080: * Returns the name of the <code>ClientMessagePartProcessor</code>.
081: * The processor will be invoked when a message part with its name
082: * is found within the ClientMessage.
083: *
084: * @return the name of the processor
085: */
086: public String getName();
087:
088: /**
089: * Processes a MessagePart of a ClientMessage
090: *
091: * @param userInstance the relevant <code>UserInstance</code>
092: * @param messagePartElement the <code>message part</code> element
093: * to process
094: */
095: public void process(UserInstance userInstance,
096: Element messagePartElement);
097: }
098:
099: /**
100: * <code>Service</code> identifier.
101: */
102: public static final String SERVICE_ID = "Echo.Synchronize";
103:
104: /**
105: * Map containing registered <code>ClientMessagePartProcessor</code>s.
106: */
107: private Map clientMessagePartProcessorMap = new HashMap();
108:
109: /**
110: * Creates a new <code>SynchronizeService</code>.
111: */
112: public SynchronizeService() {
113: super ();
114: registerClientMessagePartProcessor(new ClientAnalyzerProcessor());
115: }
116:
117: /**
118: * Trims an XML <code>InputStream</code> to work around the issue
119: * of the XML parser crashing on trailing whitespace. This issue is present
120: * with requests from Konqueror/KHTML browsers.
121: *
122: * @param in the <code>InputStream</code>
123: * @param characterEncoding the character encoding of the stream
124: * @return a cleaned version of the stream, as a
125: * <code>ByteArrayInputStream</code>.
126: */
127: private InputStream cleanXmlInputStream(InputStream in,
128: String characterEncoding) throws IOException {
129: ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
130:
131: byte[] buffer = new byte[4096];
132: int bytesRead = 0;
133:
134: try {
135: do {
136: bytesRead = in.read(buffer);
137: if (bytesRead > 0) {
138: byteOut.write(buffer, 0, bytesRead);
139: }
140: } while (bytesRead > 0);
141: } finally {
142: if (in != null) {
143: try {
144: in.close();
145: } catch (IOException ex) {
146: }
147: }
148: }
149:
150: in.close();
151:
152: byte[] data = byteOut.toByteArray();
153: data = new String(data, characterEncoding).trim().getBytes(
154: characterEncoding);
155:
156: return new ByteArrayInputStream(data);
157: }
158:
159: /**
160: * @see nextapp.echo2.webrender.Service#getId()
161: */
162: public String getId() {
163: return SERVICE_ID;
164: }
165:
166: /**
167: * @see nextapp.echo2.webrender.Service#getVersion()
168: */
169: public int getVersion() {
170: return DO_NOT_CACHE;
171: }
172:
173: /**
174: * Generates a DOM representation of the XML input POSTed to this service.
175: *
176: * @param conn the relevant <code>Connection</code>
177: * @return a DOM representation of the POSTed XML input
178: * @throws IOException if the input is invalid
179: */
180: private Document parseRequestDocument(Connection conn)
181: throws IOException {
182: HttpServletRequest request = conn.getRequest();
183: InputStream in = null;
184: try {
185: String userAgent = conn.getRequest()
186: .getHeader("user-agent");
187: if (userAgent != null
188: && userAgent.indexOf("onqueror") != -1) {
189: // Invoke XML 'cleaner', but only for user agents that contain the string "onqueror",
190: // such as Konqueror, for example.
191: in = cleanXmlInputStream(request.getInputStream(), conn
192: .getUserInstance().getCharacterEncoding());
193: } else {
194: in = request.getInputStream();
195: }
196: return DomUtil.getDocumentBuilder().parse(in);
197: } catch (SAXException ex) {
198: throw new IOException(
199: "Provided InputStream cannot be parsed: " + ex);
200: } catch (IOException ex) {
201: throw new IOException(
202: "Provided InputStream cannot be parsed: " + ex);
203: } finally {
204: if (in != null) {
205: try {
206: in.close();
207: } catch (IOException ex) {
208: }
209: }
210: }
211: }
212:
213: /**
214: * Processes a "ClientMessage" XML document containing application UI state
215: * change information from the client. This method will parse the
216: * message parts of the ClientMessage and invoke the
217: * <code>ClientMessagePartProcessor</code>s registered to process them.
218: *
219: * @param conn the relevant <code>Connection</code>
220: * @param clientMessageDocument the ClientMessage XML document to process
221: * @see ClientMessagePartProcessor
222: */
223: protected void processClientMessage(Connection conn,
224: Document clientMessageDocument) {
225: UserInstance userInstance = conn.getUserInstance();
226: Element[] messageParts = DomUtil.getChildElementsByTagName(
227: clientMessageDocument.getDocumentElement(),
228: "message-part");
229: for (int i = 0; i < messageParts.length; ++i) {
230: ClientMessagePartProcessor processor = (ClientMessagePartProcessor) clientMessagePartProcessorMap
231: .get(messageParts[i].getAttribute("processor"));
232: if (processor == null) {
233: throw new RuntimeException("Invalid processor name \""
234: + messageParts[i].getAttribute("processor")
235: + "\".");
236: }
237: processor.process(userInstance, messageParts[i]);
238: }
239: }
240:
241: /**
242: * Registers a <code>ClientMessagePartProcessor</code> to handle a
243: * specific type of message part.
244: *
245: * @param processor the <code>ClientMessagePartProcessor</code> to
246: * register
247: * @throws IllegalStateException if a processor with the same name is
248: * already registered
249: */
250: protected void registerClientMessagePartProcessor(
251: ClientMessagePartProcessor processor) {
252: if (clientMessagePartProcessorMap.containsKey(processor
253: .getName())) {
254: throw new IllegalStateException(
255: "Processor already registered with name \""
256: + processor.getName() + "\".");
257: }
258: clientMessagePartProcessorMap.put(processor.getName(),
259: processor);
260: }
261:
262: /**
263: * Renders a <code>ServerMessage</code> in response to the initial
264: * synchronization.
265: *
266: * @param conn the relevant <code>Connection</code>
267: * @param clientMessageDocument the ClientMessage XML document
268: * @return the generated <code>ServerMessage</code>
269: */
270: protected abstract ServerMessage renderInit(Connection conn,
271: Document clientMessageDocument);
272:
273: /**
274: * Renders a <code>ServerMessage</code> in response to a synchronization
275: * other than the initial synchronization.
276: *
277: * @param conn the relevant <code>Connection</code>
278: * @param clientMessageDocument the ClientMessage XML document
279: * @return the generated <code>ServerMessage</code>
280: */
281: protected abstract ServerMessage renderUpdate(Connection conn,
282: Document clientMessageDocument);
283:
284: /**
285: * @see nextapp.echo2.webrender.Service#service(nextapp.echo2.webrender.Connection)
286: */
287: public void service(Connection conn) throws IOException {
288: UserInstance userInstance = conn.getUserInstance();
289: synchronized (userInstance) {
290: Document clientMessageDocument = parseRequestDocument(conn);
291: String messageType = clientMessageDocument
292: .getDocumentElement().getAttribute("type");
293: ServerMessage serverMessage;
294:
295: if ("initialize".equals(messageType)) {
296: serverMessage = renderInit(conn, clientMessageDocument);
297: ClientPropertiesStore.renderStoreDirective(
298: serverMessage, userInstance
299: .getClientProperties());
300: ClientConfigurationUpdate.renderUpdateDirective(
301: serverMessage, userInstance
302: .getClientConfiguration());
303: ServerDelayMessageUpdate.renderUpdateDirective(
304: serverMessage, userInstance
305: .getServerDelayMessage());
306:
307: // Add "test attribute" used by ClientEngine to determine if browser is correctly (un)escaping
308: // attribute values. Safari does not do this correctly and a workaround is thus employed if such
309: // bugs are detected.
310: serverMessage.getDocument().getDocumentElement()
311: .setAttribute("xml-attr-test", "x&y");
312: } else {
313: serverMessage = renderUpdate(conn,
314: clientMessageDocument);
315: processUserInstanceUpdates(userInstance, serverMessage);
316: }
317: serverMessage.setTransactionId(userInstance
318: .getNextTransactionId());
319: conn.setContentType(ContentType.TEXT_XML);
320: serverMessage.render(conn.getWriter());
321: }
322: }
323:
324: /**
325: * Renders updates to <code>UserInstance</code> properties.
326: *
327: * @param userInstance the relevant <code>UserInstance</code>
328: * @param serverMessage the <code>ServerMessage</code> containing the updates
329: */
330: private void processUserInstanceUpdates(UserInstance userInstance,
331: ServerMessage serverMessage) {
332: UserInstanceUpdateManager updateManager = userInstance
333: .getUserInstanceUpdateManager();
334: String[] updatedPropertyNames = updateManager
335: .getPropertyUpdateNames();
336: for (int i = 0; i < updatedPropertyNames.length; ++i) {
337: if (UserInstance.PROPERTY_CLIENT_CONFIGURATION
338: .equals(updatedPropertyNames[i])) {
339: ClientConfigurationUpdate.renderUpdateDirective(
340: serverMessage, userInstance
341: .getClientConfiguration());
342: } else if (UserInstance.PROPERTY_SERVER_DELAY_MESSAGE
343: .equals(updatedPropertyNames[i])) {
344: ServerDelayMessageUpdate.renderUpdateDirective(
345: serverMessage, userInstance
346: .getServerDelayMessage());
347: }
348: }
349: updateManager.purge();
350: }
351: }
|