001: /*
002: * Copyright 2005 Joe Walker
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: */
016: package org.directwebremoting.dwrp;
017:
018: import java.io.IOException;
019: import java.io.PrintWriter;
020: import java.util.ArrayList;
021: import java.util.List;
022:
023: import javax.servlet.http.HttpServletRequest;
024: import javax.servlet.http.HttpServletResponse;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028: import org.directwebremoting.WebContextFactory;
029: import org.directwebremoting.extend.Alarm;
030: import org.directwebremoting.extend.ContainerAbstraction;
031: import org.directwebremoting.extend.ConverterManager;
032: import org.directwebremoting.extend.EnginePrivate;
033: import org.directwebremoting.extend.Handler;
034: import org.directwebremoting.extend.PageNormalizer;
035: import org.directwebremoting.extend.RealScriptSession;
036: import org.directwebremoting.extend.RealWebContext;
037: import org.directwebremoting.extend.ScriptSessionManager;
038: import org.directwebremoting.extend.ServerException;
039: import org.directwebremoting.extend.ServerLoadMonitor;
040: import org.directwebremoting.extend.Sleeper;
041: import org.directwebremoting.impl.JettyContinuationSleeper;
042: import org.directwebremoting.impl.OutputAlarm;
043: import org.directwebremoting.impl.ShutdownAlarm;
044: import org.directwebremoting.impl.TimedAlarm;
045: import org.directwebremoting.util.MimeConstants;
046:
047: /**
048: * A Marshaller that output plain Javascript.
049: * This marshaller can be tweaked to output Javascript in an HTML context.
050: * This class works in concert with CallScriptConduit, they should be
051: * considered closely related and it is important to understand what one does
052: * while editing the other.
053: * @author Joe Walker [joe at getahead dot ltd dot uk]
054: */
055: public class PollHandler implements Handler {
056: /**
057: * @param plain Are we using plain javascript or html wrapped javascript
058: */
059: public PollHandler(boolean plain) {
060: this .plain = plain;
061: }
062:
063: /* (non-Javadoc)
064: * @see org.directwebremoting.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
065: */
066: @SuppressWarnings({"ThrowableInstanceNeverThrown"})
067: public void handle(HttpServletRequest request,
068: HttpServletResponse response) throws IOException {
069: // If you're new to understanding this file, you may wish to skip this
070: // step and come back to it later ;-)
071: // So Jetty does something a bit weird with Ajax Continuations. You
072: // suspend a request (which works via an exception) while keeping hold
073: // of a continuation object. There are methods on this continuation
074: // object to restart the request. Also you can write to the output at
075: // any time the request is suspended. When the continuation is
076: // restarted, rather than restart the thread from where is was
077: // suspended, it starts it from the beginning again. Since we are able
078: // to write to the response outside of the servlet thread, there is no
079: // need for us to do anything if we have been restarted. So we ignore
080: // all Jetty continuation restarts.
081: if (JettyContinuationSleeper.isRestart(request)) {
082: JettyContinuationSleeper.restart(request);
083: return;
084: }
085:
086: // A PollBatch is the information that we expect from the request.
087: // if the parse fails we can do little more than tell the browser that
088: // something went wrong.
089: final PollBatch batch;
090: try {
091: batch = new PollBatch(request);
092: } catch (ServerException ex) {
093: // Send a batch exception to the server because the parse failed
094: String script = EnginePrivate
095: .getRemoteHandleBatchExceptionScript(null, ex);
096: sendErrorScript(response, script);
097: return;
098: }
099:
100: // Check to see that the page and script session id are valid
101: RealWebContext webContext = (RealWebContext) WebContextFactory
102: .get();
103: String normalizedPage = pageNormalizer.normalizePage(batch
104: .getPage());
105: webContext.checkPageInformation(normalizedPage, batch
106: .getScriptSessionId(), batch.getWindowName());
107:
108: // We might need to complain that reverse ajax is not enabled.
109: if (!activeReverseAjaxEnabled) {
110: log
111: .error("Polling and Comet are disabled. To enable them set the init-param activeReverseAjaxEnabled to true. See http://getahead.org/dwr/server/servlet for more.");
112: String script = EnginePrivate
113: .getRemotePollCometDisabledScript(batch
114: .getBatchId());
115: sendErrorScript(response, script);
116: return;
117: }
118:
119: // Complain if GET is disallowed
120: if (batch.isGet() && !allowGetForSafariButMakeForgeryEasier) {
121: // Send a batch exception to the server because the parse failed
122: String script = EnginePrivate
123: .getRemoteHandleBatchExceptionScript(batch
124: .getBatchId(), new SecurityException(
125: "GET Disallowed"));
126: sendErrorScript(response, script);
127: return;
128: }
129:
130: // A script conduit is some route from a ScriptSession back to the page
131: // that belongs to the session. There may be zero or many of these
132: // conduits (although if there are more than 2, something is strange)
133: // All scripts destined for a page go to a ScriptSession and then out
134: // via a ScriptConduit.
135: final RealScriptSession scriptSession = (RealScriptSession) webContext
136: .getScriptSession();
137:
138: // Create a conduit depending on the type of request (from the URL)
139: final BaseScriptConduit conduit = createScriptConduit(batch,
140: response);
141:
142: // So we're going to go to sleep. How do we wake up?
143: Sleeper sleeper = containerAbstraction.createSleeper(request);
144:
145: // There are various reasons why we want to wake up and carry on ...
146: final List<Alarm> alarms = new ArrayList<Alarm>();
147:
148: // If the conduit has an error flushing data, it needs to give up
149: alarms.add(conduit.getErrorAlarm());
150:
151: // Set the system up to resume on output (perhaps with delay)
152: if (batch.getPartialResponse() == PartialResponse.NO
153: || maxWaitAfterWrite != -1) {
154: // add an output listener to the script session that calls the
155: // "wake me" method on whatever is putting us to sleep
156: alarms
157: .add(new OutputAlarm(scriptSession,
158: maxWaitAfterWrite));
159: }
160:
161: // Set the system up to resume anyway after maxConnectedTime
162: long maxConnectedTime = serverLoadMonitor.getConnectedTime();
163: alarms.add(new TimedAlarm(maxConnectedTime));
164:
165: // We also need to wake-up if the server is being shut down
166: // WARNING: This code has a non-obvious side effect - The server load
167: // monitor (which hands out shutdown messages) also monitors usage by
168: // looking at the number of connected alarms.
169: alarms.add(new ShutdownAlarm(serverLoadMonitor));
170:
171: // Make sure that all the alarms know what to wake
172: for (Alarm alarm : alarms) {
173: alarm.setAlarmAction(sleeper);
174: }
175:
176: // We need to do something sensible when we wake up ...
177: Runnable onAwakening = new Runnable() {
178: public void run() {
179: // Cancel all the alarms
180: for (Alarm alarm : alarms) {
181: alarm.cancel();
182: }
183:
184: // We can't be used as a conduit to the browser any more
185: scriptSession.removeScriptConduit(conduit);
186:
187: // Tell the browser to come back at the right time
188: try {
189: int timeToNextPoll = serverLoadMonitor
190: .getDisconnectedTime();
191: conduit.close(timeToNextPoll);
192: } catch (IOException ex) {
193: log
194: .warn("Failed to write reconnect info to browser");
195: }
196: }
197: };
198:
199: // Register the conduit with a script session so messages can get out.
200: // This must happen late on in this method because this will cause any
201: // scripts cached in the script session (because there was no conduit
202: // available when they were written) to be sent to the conduit.
203: // We need any AlarmScriptConduits to be notified so they can make
204: // maxWaitWfterWrite work for all cases
205: scriptSession.addScriptConduit(conduit);
206:
207: // Actually go to sleep. This *must* be the last thing in this method to
208: // cope with all the methods of affecting Threads. Jetty throws,
209: // Weblogic continues, others wait().
210: sleeper.goToSleep(onAwakening);
211: }
212:
213: /**
214: * Create the correct type of ScriptConduit depending on the request.
215: * @param batch The parsed request
216: * @param response Conduits need a response to write to
217: * @return A correctly configured conduit
218: * @throws IOException If the response can't be interrogated
219: */
220: private BaseScriptConduit createScriptConduit(PollBatch batch,
221: HttpServletResponse response) throws IOException {
222: BaseScriptConduit conduit;
223:
224: if (plain) {
225: conduit = new PlainScriptConduit(response, batch
226: .getBatchId(), converterManager, jsonOutput);
227: } else {
228: if (batch.getPartialResponse() == PartialResponse.FLUSH) {
229: conduit = new Html4kScriptConduit(response, batch
230: .getBatchId(), converterManager, jsonOutput);
231: } else {
232: conduit = new HtmlScriptConduit(response, batch
233: .getBatchId(), converterManager, jsonOutput);
234: }
235: }
236:
237: return conduit;
238: }
239:
240: /**
241: * Send a script to the browser and wrap it in the required prefixes etc.
242: * @param response The http response to write to
243: * @param script The script to write
244: * @throws IOException if writing fails.
245: */
246: protected void sendErrorScript(HttpServletResponse response,
247: String script) throws IOException {
248: PrintWriter out = response.getWriter();
249: if (plain) {
250: response.setContentType(MimeConstants.MIME_PLAIN);
251: } else {
252: response.setContentType(MimeConstants.MIME_HTML);
253: }
254:
255: out.println(ProtocolConstants.SCRIPT_START_MARKER);
256: out.println(script);
257: out.println(ProtocolConstants.SCRIPT_END_MARKER);
258: }
259:
260: /**
261: * Accessor for the DefaultCreatorManager that we configure
262: * @param converterManager The new DefaultConverterManager
263: */
264: public void setConverterManager(ConverterManager converterManager) {
265: this .converterManager = converterManager;
266: }
267:
268: /**
269: * Accessor for the server load monitor
270: * @param serverLoadMonitor the new server load monitor
271: */
272: public void setServerLoadMonitor(ServerLoadMonitor serverLoadMonitor) {
273: this .serverLoadMonitor = serverLoadMonitor;
274: }
275:
276: /**
277: * Accessor for the PageNormalizer.
278: * @param pageNormalizer The new PageNormalizer
279: */
280: public void setPageNormalizer(PageNormalizer pageNormalizer) {
281: this .pageNormalizer = pageNormalizer;
282: }
283:
284: /**
285: * @param scriptSessionManager the scriptSessionManager to set
286: */
287: public void setScriptSessionManager(
288: ScriptSessionManager scriptSessionManager) {
289: this .scriptSessionManager = scriptSessionManager;
290: }
291:
292: /**
293: * Use {@link #setActiveReverseAjaxEnabled(boolean)}
294: * @param pollAndCometEnabled Are we doing full reverse ajax
295: * @deprecated Use {@link #setActiveReverseAjaxEnabled(boolean)}
296: */
297: @Deprecated
298: public void setPollAndCometEnabled(boolean pollAndCometEnabled) {
299: this .activeReverseAjaxEnabled = pollAndCometEnabled;
300: }
301:
302: /**
303: * Are we doing full reverse ajax
304: * @param activeReverseAjaxEnabled Are we doing full reverse ajax
305: */
306: public void setActiveReverseAjaxEnabled(
307: boolean activeReverseAjaxEnabled) {
308: this .activeReverseAjaxEnabled = activeReverseAjaxEnabled;
309: }
310:
311: /**
312: * @param allowGetForSafariButMakeForgeryEasier Do we reduce security to help Safari
313: */
314: public void setAllowGetForSafariButMakeForgeryEasier(
315: boolean allowGetForSafariButMakeForgeryEasier) {
316: this .allowGetForSafariButMakeForgeryEasier = allowGetForSafariButMakeForgeryEasier;
317: }
318:
319: /**
320: * @param containerAbstraction the containerAbstraction to set
321: */
322: public void setContainerAbstraction(
323: ContainerAbstraction containerAbstraction) {
324: this .containerAbstraction = containerAbstraction;
325: }
326:
327: /**
328: * Sometimes with proxies, you need to close the stream all the time to
329: * make the flush work. A value of -1 indicated that we do not do early
330: * closing after writes.
331: * @param maxWaitAfterWrite the maxWaitAfterWrite to set
332: */
333: public void setMaxWaitAfterWrite(int maxWaitAfterWrite) {
334: this .maxWaitAfterWrite = maxWaitAfterWrite;
335: }
336:
337: /**
338: * @return Are we outputting in JSON mode?
339: */
340: public boolean isJsonOutput() {
341: return jsonOutput;
342: }
343:
344: /**
345: * @param jsonOutput Are we outputting in JSON mode?
346: */
347: public void setJsonOutput(boolean jsonOutput) {
348: this .jsonOutput = jsonOutput;
349: }
350:
351: /**
352: * Are we outputting in JSON mode?
353: */
354: protected boolean jsonOutput = false;
355:
356: /**
357: * Are we doing full reverse ajax
358: */
359: protected boolean activeReverseAjaxEnabled = false;
360:
361: /**
362: * By default we disable GET, but this hinders old Safaris
363: */
364: protected boolean allowGetForSafariButMakeForgeryEasier = false;
365:
366: /**
367: * Sometimes with proxies, you need to close the stream all the time to
368: * make the flush work. A value of -1 indicated that we do not do early
369: * closing after writes.
370: * See also: org.directwebremoting.servlet.FileHandler.maxWaitAfterWrite
371: */
372: protected int maxWaitAfterWrite = -1;
373:
374: /**
375: * Are we using plain javascript or html wrapped javascript
376: */
377: protected boolean plain;
378:
379: /**
380: * How we turn pages into the canonical form.
381: */
382: protected PageNormalizer pageNormalizer;
383:
384: /**
385: * We need to tell the system that we are waiting so it can load adjust
386: */
387: protected ServerLoadMonitor serverLoadMonitor = null;
388:
389: /**
390: * How we convert parameters
391: */
392: protected ConverterManager converterManager = null;
393:
394: /**
395: * The owner of script sessions
396: */
397: protected ScriptSessionManager scriptSessionManager = null;
398:
399: /**
400: * How we abstract away container specific logic
401: */
402: protected ContainerAbstraction containerAbstraction = null;
403:
404: /**
405: * The log stream
406: */
407: protected static final Log log = LogFactory
408: .getLog(PollHandler.class);
409: }
|