001: /*
002: * <copyright>
003: *
004: * Copyright 2000-2007 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026:
027: package org.cougaar.core.plugin;
028:
029: import java.io.IOException;
030: import java.io.UnsupportedEncodingException;
031: import java.lang.reflect.InvocationTargetException;
032: import java.net.URLEncoder;
033: import java.net.URLDecoder;
034: import java.util.Collection;
035: import java.util.Iterator;
036: import javax.servlet.ServletException;
037: import javax.servlet.http.HttpServlet;
038: import javax.servlet.http.HttpServletRequest;
039: import javax.servlet.http.HttpServletResponse;
040:
041: import org.cougaar.bootstrap.SystemProperties;
042: import org.cougaar.core.blackboard.Subscription;
043: import org.cougaar.core.blackboard.TodoSubscription;
044: import org.cougaar.core.service.ServletService;
045: import org.cougaar.util.Arguments;
046: import org.cougaar.util.FutureResult;
047: import org.cougaar.util.UnaryPredicate;
048:
049: /**
050: * This plugin is a base class for servlets that either modify or subscribe
051: * to the blackboard.
052: * <p>
053: * If a servlet only requires blackboard queries and no subscriptions or
054: * add/change/remove modifications, then {@link ComponentServlet} is
055: * recommended.
056: * <p>
057: * If {@link #isTransactional} is true then all servlet processing work is
058: * done single-threaded in the "execute()" method, where it can access
059: * subscriptions and modify the blackboard using the standard
060: * "blackboard.publish*" methods.
061: *
062: * @param org.cougaar.core.servlet.ServletPlugin.timeout=60000
063: * Default timeout for ServletPlugin requests, which are processed
064: * in the plugin's "execute()" thread.
065: */
066: public abstract class ServletPlugin extends ComponentPlugin {
067:
068: private static final long DEFAULT_TIMEOUT = SystemProperties
069: .getLong("org.cougaar.core.servlet.ServletPlugin.timeout",
070: 60000);
071:
072: private String path;
073: private boolean isTrans;
074: private long timeout;
075: private String encAgentName;
076:
077: private ServletService servletService;
078:
079: private TodoSubscription todo;
080:
081: public ServletPlugin() {
082: super ();
083: }
084:
085: /**
086: * Return true (the default) if all servlet requests should run
087: * single-threaded in the "execute()" method, otherwise false if they should
088: * run in the servlet engine's threads (and possibly in parallel with other
089: * servlet requests and the "execute()" thread).
090: * <p>
091: * If a servlet requires a cross-transaction result, e.g.:<ol>
092: * <li>publishAdd object X</li>
093: * <li>wait another plugin to react to object X, e.g. via a
094: * "notify()" callback or published response</li>
095: * <li>finish the servlet call</li>
096: * </ol>
097: * then "isTransactional()" must return false.
098: * <p>
099: * Also, if a servlet has no subscriptions or private state, then it's
100: * slightly more efficient to have this method return false. This will allow
101: * requests to run in parallel.
102: * <p>
103: * In all other cases, this method should return true. This allows the
104: * "doGet(...)" method to access subscriptions and other internal state
105: * without a synchronization lock, since the "execute()" method is always
106: * single-threaded and runs in a blackboard transaction.
107: */
108: protected boolean isTransactional() {
109: return true;
110: }
111:
112: /**
113: * Get the path for the Servlet's registration.
114: * <p>
115: * Typically the path is supplied by a "path=" plugin argument, but a
116: * subclass can hard-code the path by overriding this method.
117: */
118: protected String getPath() {
119: return path;
120: }
121:
122: public void load() {
123: super .load();
124:
125: Collection params = getParameters();
126: Arguments args = new Arguments(params);
127:
128: // set threading
129: isTrans = isTransactional();
130:
131: // set path
132: path = getPath();
133: if (path == null) {
134: path = args.getString("path");
135: if (path == null && !params.isEmpty()) {
136: String s = (String) params.iterator().next();
137: if (s.indexOf('=') < 0) {
138: path = s;
139: }
140: }
141: if (path == null) {
142: throw new IllegalArgumentException(
143: "Missing path parameter");
144: }
145: }
146:
147: // set timeout
148: timeout = args.getLong("timeout", DEFAULT_TIMEOUT);
149:
150: // get encoded agent name
151: String agentName = (agentId == null ? null : agentId
152: .getAddress());
153: encAgentName = (agentName == null ? null : encode(agentName));
154:
155: // get our servlet service
156: servletService = (ServletService) getServiceBroker()
157: .getService(this , ServletService.class, null);
158: if (servletService == null) {
159: throw new RuntimeException(
160: "Unable to obtain ServletService");
161: }
162:
163: // register our servlet
164: try {
165: HttpServlet servlet = createServlet();
166: servletService.register(path, servlet);
167: } catch (Exception e) {
168: throw new RuntimeException("Unable to register " + path, e);
169: }
170: }
171:
172: public void unload() {
173: if (servletService != null) {
174: // this will automatically unregister our servlet
175: getServiceBroker().releaseService(this ,
176: ServletService.class, servletService);
177: servletService = null;
178: }
179:
180: super .unload();
181: }
182:
183: /** Get the URL-encoded name of the local agent */
184: protected String getEncodedAgentName() {
185: return encAgentName;
186: }
187:
188: protected void setupSubscriptions() {
189: if (isTrans) {
190: todo = (TodoSubscription) blackboard
191: .subscribe(new TodoSubscription("x"));
192: }
193: }
194:
195: protected void execute() {
196: if (!isTrans)
197: return;
198: ensureTodo();
199: if (!todo.hasChanged())
200: return;
201: for (Iterator iter = todo.getAddedCollection().iterator(); iter
202: .hasNext();) {
203: HttpJob job = (HttpJob) iter.next();
204: try {
205: service(job.getHttpServletRequest(), job
206: .getHttpServletResponse());
207: job.notifySuccess();
208: } catch (Exception e) {
209: job.notifyFailure(e);
210: }
211: }
212: }
213:
214: private void ensureTodo() {
215: if (todo == null) {
216: throw new RuntimeException(
217: "The \"todo\" subscription is null. Is \"setupSubscriptions()\""
218: + " missing a call to \"super.setupSubscriptions()\"?");
219: }
220: }
221:
222: protected Subscription subscribe(UnaryPredicate pred) {
223: if (!isTrans || !blackboard.isTransactionOpen()) {
224: throw new IllegalStateException(
225: "Can only subscribe if \"isTransactional()\" is true");
226: }
227: return blackboard.subscribe(pred);
228: }
229:
230: protected Collection query(UnaryPredicate pred) {
231: if (isTrans || blackboard.isTransactionOpen()) {
232: return blackboard.query(pred);
233: } else {
234: blackboard.openTransaction();
235: Collection c = blackboard.query(pred);
236: blackboard.closeTransactionDontReset();
237: return c;
238: }
239: }
240:
241: protected void publishAdd(Object o) {
242: if (isTrans || blackboard.isTransactionOpen()) {
243: blackboard.publishAdd(o);
244: } else {
245: blackboard.openTransaction();
246: blackboard.publishAdd(o);
247: blackboard.closeTransactionDontReset();
248: }
249: }
250:
251: protected void publishChange(Object o) {
252: publishChange(o, null);
253: }
254:
255: protected void publishChange(Object o, Collection changes) {
256: if (isTrans || blackboard.isTransactionOpen()) {
257: blackboard.publishChange(o, changes);
258: } else {
259: blackboard.openTransaction();
260: blackboard.publishChange(o, changes);
261: blackboard.closeTransactionDontReset();
262: }
263: }
264:
265: protected void publishRemove(Object o) {
266: if (isTrans || blackboard.isTransactionOpen()) {
267: blackboard.publishRemove(o);
268: } else {
269: blackboard.openTransaction();
270: blackboard.publishRemove(o);
271: blackboard.closeTransactionDontReset();
272: }
273: }
274:
275: protected void serviceLater(HttpServletRequest req,
276: HttpServletResponse resp) throws ServletException,
277: IOException {
278: if (!isTrans) {
279: throw new IllegalStateException(
280: "Can only call \"serviceLater(...)\" if \"isTransactional()\" is"
281: + " true.");
282: }
283: ensureTodo();
284:
285: // put on our "todo" queue
286: HttpJob job = new HttpJob(req, resp);
287: todo.add(job);
288:
289: // wait for the result, which will rethrow any "notifyFailure" exception
290: job.waitForNotify(timeout);
291: }
292:
293: /**
294: * Create our servlet, which by default calls our doGet/doPost/doPost
295: * methods.
296: * <p>
297: * A subclass can override this method if it's designed to create a separate
298: * servlet. However, if {@link #isTransactional} is true then it should use
299: * {@link #serviceLater} to do all blackboard work in the "execute()" method
300: * instead of the servlet callback thread.
301: * <p>
302: * Even though we switch to the "execute()" thread, we must still block
303: * the servlet callback until we finish the work, otherwise the servlet engine
304: * will close our response stream.
305: */
306: protected HttpServlet createServlet() {
307: return new HttpServlet() {
308: protected void service(HttpServletRequest req,
309: HttpServletResponse resp) throws ServletException,
310: IOException {
311: if (isTrans) {
312: serviceLater(req, resp);
313: } else {
314: ServletPlugin.this .service(req, resp);
315: }
316: }
317: };
318: }
319:
320: /** Basic servlet methods */
321: protected void service(HttpServletRequest req,
322: HttpServletResponse resp) throws ServletException,
323: IOException {
324: String method = req.getMethod();
325: if ("GET".equals(method)) {
326: doGet(req, resp);
327: } else if ("POST".equals(method)) {
328: doPost(req, resp);
329: } else if ("PUT".equals(method)) {
330: doPut(req, resp);
331: } else if ("OPTIONS".equals(method)) {
332: // RFE do same thing as in HttpServlet
333: resp.setHeader("Allow", "GET, HEAD, POST, PUT, OPTIONS");
334: } else {
335: notSupported(req, resp);
336: }
337: }
338:
339: protected void doGet(HttpServletRequest req,
340: HttpServletResponse resp) throws ServletException,
341: IOException {
342: notSupported(req, resp);
343: }
344:
345: protected void doPost(HttpServletRequest req,
346: HttpServletResponse resp) throws ServletException,
347: IOException {
348: notSupported(req, resp);
349: }
350:
351: protected void doPut(HttpServletRequest req,
352: HttpServletResponse resp) throws ServletException,
353: IOException {
354: notSupported(req, resp);
355: }
356:
357: protected void notSupported(HttpServletRequest req,
358: HttpServletResponse resp) throws IOException {
359: String method = req.getMethod();
360: String msg = "HTTP method " + method
361: + " is not supported by this URL";
362: String protocol = req.getProtocol();
363: int sc = (protocol != null && protocol.endsWith("1.1") ? HttpServletResponse.SC_METHOD_NOT_ALLOWED
364: : HttpServletResponse.SC_BAD_REQUEST);
365: resp.sendError(sc, msg);
366: }
367:
368: /** @return the UTF-8 encoded string */
369: protected String encode(String s) {
370: try {
371: return (s == null ? null : URLEncoder.encode(s, "UTF-8"));
372: } catch (UnsupportedEncodingException e) {
373: // should never happen
374: throw new RuntimeException("Unable to encode to UTF-8?");
375: }
376: }
377:
378: /** @return the UTF-8 decoded string */
379: protected String decode(String s) {
380: try {
381: return (s == null ? null : URLDecoder.decode(s, "UTF-8"));
382: } catch (UnsupportedEncodingException e) {
383: // should never happen
384: throw new RuntimeException("Unable to decode to UTF-8?");
385: }
386: }
387:
388: protected static final class HttpJob {
389:
390: private final HttpServletRequest req;
391: private final HttpServletResponse resp;
392: private final FutureResult future = new FutureResult();
393:
394: public HttpJob(HttpServletRequest req, HttpServletResponse resp) {
395: this .req = req;
396: this .resp = resp;
397: }
398:
399: public HttpServletRequest getHttpServletRequest() {
400: return req;
401: }
402:
403: public HttpServletResponse getHttpServletResponse() {
404: return resp;
405: }
406:
407: public void notifySuccess() {
408: future.set(Boolean.TRUE);
409: }
410:
411: public void notifyFailure(Throwable t) {
412: future.setException(t);
413: }
414:
415: public void waitForNotify(long timeout)
416: throws ServletException, IOException {
417: // wait for the result
418: Throwable t;
419: try {
420: Object o = future.timedGet(timeout);
421: if (o == Boolean.TRUE) {
422: // success
423: return;
424: }
425: t = new InternalError("Unexpected submit result: " + o);
426: } catch (InvocationTargetException ite) {
427: t = ite.getCause();
428: } catch (InterruptedException ie) {
429: t = ie;
430: }
431:
432: // rethrow the exception
433: if (t instanceof RuntimeException) {
434: throw (RuntimeException) t;
435: } else if (t instanceof ServletException) {
436: throw (ServletException) t;
437: } else if (t instanceof IOException) {
438: throw (IOException) t;
439: } else if (t instanceof InterruptedException) {
440: throw new RuntimeException("Request timeout");
441: } else if (t instanceof Error) {
442: throw (Error) t;
443: } else {
444: throw new RuntimeException("Wrapped exception", t);
445: }
446: }
447: }
448: }
|