001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common Development
008: * and Distribution License("CDDL") (collectively, the "License"). You
009: * may not use this file except in compliance with the License. You can obtain
010: * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
011: * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
012: * language governing permissions and limitations under the License.
013: *
014: * When distributing the software, include this License Header Notice in each
015: * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
016: * Sun designates this particular file as subject to the "Classpath" exception
017: * as provided by Sun in the GPL Version 2 section of the License file that
018: * accompanied this code. If applicable, add the following below the License
019: * Header, with the fields enclosed by brackets [] replaced by your own
020: * identifying information: "Portions Copyrighted [year]
021: * [name of copyright owner]"
022: *
023: * Contributor(s):
024: *
025: * If you wish your version of this file to be governed by only the CDDL or
026: * only the GPL Version 2, indicate your decision by adding "[Contributor]
027: * elects to include this software in this distribution under the [CDDL or GPL
028: * Version 2] license." If you don't indicate a single choice of license, a
029: * recipient has the option to distribute your version of this file under
030: * either the CDDL, the GPL Version 2 or to extend the choice of license to
031: * its licensees as provided above. However, if you add GPL Version 2 code
032: * and therefore, elected the GPL Version 2 license, then the option applies
033: * only if the new code is made subject to such option by the copyright
034: * holder.
035: */
036:
037: package com.sun.xml.ws.server;
038:
039: import com.sun.istack.FragmentContentHandler;
040: import com.sun.istack.NotNull;
041: import com.sun.istack.Nullable;
042: import com.sun.xml.stream.buffer.XMLStreamBufferSource;
043: import com.sun.xml.stream.buffer.stax.StreamWriterBufferCreator;
044: import com.sun.xml.ws.api.addressing.AddressingVersion;
045: import com.sun.xml.ws.api.message.Header;
046: import com.sun.xml.ws.api.message.HeaderList;
047: import com.sun.xml.ws.api.message.Packet;
048: import com.sun.xml.ws.api.server.InstanceResolver;
049: import com.sun.xml.ws.api.server.WSEndpoint;
050: import com.sun.xml.ws.api.server.WSWebServiceContext;
051: import com.sun.xml.ws.developer.EPRRecipe;
052: import com.sun.xml.ws.developer.StatefulWebServiceManager;
053: import com.sun.xml.ws.resources.ServerMessages;
054: import com.sun.xml.ws.spi.ProviderImpl;
055: import com.sun.xml.ws.util.xml.ContentHandlerToXMLStreamWriter;
056: import com.sun.xml.ws.util.xml.XmlUtil;
057: import org.xml.sax.Attributes;
058: import org.xml.sax.SAXException;
059: import org.xml.sax.helpers.DefaultHandler;
060:
061: import javax.xml.namespace.QName;
062: import javax.xml.stream.XMLStreamException;
063: import javax.xml.transform.Source;
064: import javax.xml.transform.Transformer;
065: import javax.xml.transform.TransformerException;
066: import javax.xml.transform.sax.SAXResult;
067: import javax.xml.ws.EndpointReference;
068: import javax.xml.ws.WebServiceContext;
069: import javax.xml.ws.WebServiceException;
070: import javax.xml.ws.wsaddressing.W3CEndpointReference;
071: import java.lang.reflect.Field;
072: import java.lang.reflect.Method;
073: import java.lang.reflect.Modifier;
074: import java.util.Collections;
075: import java.util.HashMap;
076: import java.util.List;
077: import java.util.Map;
078: import java.util.Timer;
079: import java.util.TimerTask;
080: import java.util.UUID;
081: import java.util.logging.Level;
082: import java.util.logging.Logger;
083:
084: /**
085: * {@link InstanceResolver} that looks at JAX-WS cookie header to
086: * determine the instance to which a message will be routed.
087: *
088: * <p>
089: * See {@link StatefulWebServiceManager} for more about user-level semantics.
090: *
091: * @author Kohsuke Kawaguchi
092: */
093: public final class StatefulInstanceResolver<T> extends
094: AbstractMultiInstanceResolver<T> implements
095: StatefulWebServiceManager<T> {
096: /**
097: * This instance is used for serving messages that have no cookie
098: * or cookie value that the server doesn't recognize.
099: */
100: private volatile @Nullable
101: T fallback;
102:
103: /**
104: * Maintains the stateful service instance and its time-out timer.
105: */
106: private final class Instance {
107: final @NotNull
108: T instance;
109: TimerTask task;
110:
111: public Instance(T instance) {
112: this .instance = instance;
113: }
114:
115: /**
116: * Resets the timer.
117: */
118: public synchronized void restartTimer() {
119: cancel();
120: if (timeoutMilliseconds == 0)
121: return; // no timer
122:
123: task = new TimerTask() {
124: public void run() {
125: try {
126: Callback<T> cb = timeoutCallback;
127: if (cb != null) {
128: cb.onTimeout(instance,
129: StatefulInstanceResolver.this );
130: return;
131: }
132: // default operation is to unexport it.
133: unexport(instance);
134: } catch (Throwable e) {
135: // don't let an error in the code kill the timer thread
136: logger.log(Level.SEVERE,
137: "time out handler failed", e);
138: }
139: }
140: };
141: timer.schedule(task, timeoutMilliseconds);
142: }
143:
144: /**
145: * Cancels the timer.
146: */
147: public synchronized void cancel() {
148: if (task != null)
149: task.cancel();
150: task = null;
151: }
152: }
153:
154: /**
155: * Maps object ID to instances.
156: */
157: private final Map<String, Instance> instances = Collections
158: .synchronizedMap(new HashMap<String, Instance>());
159: /**
160: * Reverse look up for {@link #instances}.
161: */
162: private final Map<T, String> reverseInstances = Collections
163: .synchronizedMap(new HashMap<T, String>());
164:
165: // time out control. 0=disabled
166: private volatile long timeoutMilliseconds = 0;
167: private volatile Callback<T> timeoutCallback;
168:
169: public StatefulInstanceResolver(Class<T> clazz) {
170: super (clazz);
171: }
172:
173: @Override
174: public @NotNull
175: T resolve(Packet request) {
176: HeaderList headers = request.getMessage().getHeaders();
177: Header header = headers.get(COOKIE_TAG, true);
178: String id = null;
179: if (header != null) {
180: // find the instance
181: id = header.getStringContent();
182: Instance o = instances.get(id);
183: if (o != null) {
184: o.restartTimer();
185: return o.instance;
186: }
187:
188: // huh? what is this ID?
189: logger.log(Level.INFO,
190: "Request had an unrecognized object ID " + id);
191: }
192:
193: // need to fallback
194: T fallback = this .fallback;
195: if (fallback != null)
196: return fallback;
197:
198: if (id == null)
199: throw new WebServiceException(ServerMessages
200: .STATEFUL_COOKIE_HEADER_REQUIRED(COOKIE_TAG));
201: else
202: throw new WebServiceException(ServerMessages
203: .STATEFUL_COOKIE_HEADER_INCORRECT(COOKIE_TAG, id));
204: }
205:
206: @Override
207: public void start(WSWebServiceContext wsc, WSEndpoint endpoint) {
208: super .start(wsc, endpoint);
209:
210: if (endpoint.getBinding().getAddressingVersion() == null)
211: // addressing is not enabled.
212: throw new WebServiceException(ServerMessages
213: .STATEFUL_REQURES_ADDRESSING(clazz));
214:
215: // inject StatefulWebServiceManager.
216: for (Field field : clazz.getDeclaredFields()) {
217: if (field.getType() == StatefulWebServiceManager.class) {
218: if (!Modifier.isStatic(field.getModifiers()))
219: throw new WebServiceException(ServerMessages
220: .STATIC_RESOURCE_INJECTION_ONLY(
221: StatefulWebServiceManager.class,
222: field));
223: new FieldInjectionPlan<T, StatefulWebServiceManager>(
224: field).inject(null, this );
225: }
226: }
227:
228: for (Method method : clazz.getDeclaredMethods()) {
229: Class[] paramTypes = method.getParameterTypes();
230: if (paramTypes.length != 1)
231: continue; // not what we are looking for
232:
233: if (paramTypes[0] == StatefulWebServiceManager.class) {
234: if (!Modifier.isStatic(method.getModifiers()))
235: throw new WebServiceException(ServerMessages
236: .STATIC_RESOURCE_INJECTION_ONLY(
237: StatefulWebServiceManager.class,
238: method));
239:
240: new MethodInjectionPlan<T, StatefulWebServiceManager>(
241: method).inject(null, this );
242: }
243: }
244: }
245:
246: @Override
247: public void dispose() {
248: reverseInstances.clear();
249: synchronized (instances) {
250: for (Instance t : instances.values()) {
251: t.cancel();
252: dispose(t.instance);
253: }
254: instances.clear();
255: }
256: if (fallback != null)
257: dispose(fallback);
258: fallback = null;
259: }
260:
261: @NotNull
262: public W3CEndpointReference export(T o) {
263: return export(W3CEndpointReference.class, o);
264: }
265:
266: @NotNull
267: public <EPR extends EndpointReference> EPR export(Class<EPR> epr,
268: T o) {
269: return export(epr, o, null);
270: }
271:
272: public <EPR extends EndpointReference> EPR export(Class<EPR> epr,
273: T o, EPRRecipe recipe) {
274: return export(epr, InvokerTube.getCurrentPacket(), o, recipe);
275: }
276:
277: @NotNull
278: public <EPR extends EndpointReference> EPR export(Class<EPR> epr,
279: WebServiceContext context, T o) {
280: if (context instanceof WSWebServiceContext) {
281: WSWebServiceContext wswsc = (WSWebServiceContext) context;
282: return export(epr, wswsc.getRequestPacket(), o);
283: }
284:
285: throw new WebServiceException(ServerMessages
286: .STATEFUL_INVALID_WEBSERVICE_CONTEXT(context));
287: }
288:
289: @NotNull
290: public <EPR extends EndpointReference> EPR export(
291: Class<EPR> adrsVer, @NotNull
292: Packet currentRequest, T o) {
293: return export(adrsVer, currentRequest, o, null);
294: }
295:
296: public <EPR extends EndpointReference> EPR export(
297: Class<EPR> adrsVer, @NotNull
298: Packet currentRequest, T o, EPRRecipe recipe) {
299: return export(adrsVer, currentRequest.webServiceContextDelegate
300: .getEPRAddress(currentRequest, owner), o, recipe);
301: }
302:
303: @NotNull
304: public <EPR extends EndpointReference> EPR export(
305: Class<EPR> adrsVer, String endpointAddress, T o) {
306: return export(adrsVer, endpointAddress, o, null);
307: }
308:
309: @NotNull
310: public <EPR extends EndpointReference> EPR export(
311: Class<EPR> adrsVer, String endpointAddress, T o,
312: EPRRecipe recipe) {
313: if (endpointAddress == null)
314: throw new IllegalArgumentException("No address available");
315:
316: String key = reverseInstances.get(o);
317:
318: if (key != null)
319: return createEPR(key, adrsVer, endpointAddress, recipe);
320:
321: // not exported yet.
322: synchronized (this ) {
323: // double check now in the synchronization block to
324: // really make sure that we can export.
325: key = reverseInstances.get(o);
326: if (key != null)
327: return createEPR(key, adrsVer, endpointAddress, recipe);
328:
329: if (o != null)
330: prepare(o);
331: key = UUID.randomUUID().toString();
332: Instance instance = new Instance(o);
333: instances.put(key, instance);
334: reverseInstances.put(o, key);
335: if (timeoutMilliseconds != 0)
336: instance.restartTimer();
337: }
338:
339: return createEPR(key, adrsVer, endpointAddress, recipe);
340: }
341:
342: /**
343: * Creates an EPR that has the right key.
344: */
345: private <EPR extends EndpointReference> EPR createEPR(String key,
346: Class<EPR> eprClass, String address, EPRRecipe recipe) {
347: AddressingVersion adrsVer = AddressingVersion
348: .fromSpecClass(eprClass);
349:
350: try {
351: StreamWriterBufferCreator w = new StreamWriterBufferCreator();
352:
353: w.writeStartDocument();
354: w.writeStartElement("wsa", "EndpointReference",
355: adrsVer.nsUri);
356: w.writeNamespace("wsa", adrsVer.nsUri);
357:
358: w.writeStartElement("wsa", "Address", adrsVer.nsUri);
359: w.writeCharacters(address);
360: w.writeEndElement();
361:
362: w.writeStartElement("wsa", "ReferenceParameters",
363: adrsVer.nsUri);
364: w.writeStartElement(COOKIE_TAG.getPrefix(), COOKIE_TAG
365: .getLocalPart(), COOKIE_TAG.getNamespaceURI());
366: w.writeCharacters(key);
367: w.writeEndElement();
368: if (recipe != null) {
369: for (Header h : recipe.getReferenceParameters())
370: h.writeTo(w);
371: }
372: w.writeEndElement();
373:
374: if (recipe != null) {
375: List<Source> metadata = recipe.getMetadata();
376: if (!metadata.isEmpty()) {
377: w.writeStartElement("wsa", "Metadata",
378: adrsVer.nsUri);
379: Transformer t = XmlUtil.newTransformer();
380: for (Source s : metadata)
381: try {
382: t
383: .transform(
384: s,
385: new SAXResult(
386: new FragmentContentHandler(
387: new ContentHandlerToXMLStreamWriter(
388: w))));
389: } catch (TransformerException e) {
390: throw new IllegalArgumentException(
391: "Unable to write EPR metadata " + s,
392: e);
393: }
394: w.writeEndElement();
395: }
396: }
397:
398: w.writeEndElement();
399: w.writeEndDocument();
400:
401: // TODO: this can be done better by writing SAX code that produces infoset
402: // and setting that as Source.
403: return eprClass.cast(ProviderImpl.INSTANCE
404: .readEndpointReference(new XMLStreamBufferSource(w
405: .getXMLStreamBuffer())));
406: } catch (XMLStreamException e) {
407: throw new Error(e); // this must be a bug in our code
408: }
409: }
410:
411: public void unexport(@Nullable
412: T o) {
413: if (o == null)
414: return;
415: String key = reverseInstances.get(o);
416: if (key == null)
417: return; // already unexported
418: instances.remove(key);
419: reverseInstances.remove(o);
420: }
421:
422: public T resolve(EndpointReference epr) {
423: class CookieSniffer extends DefaultHandler {
424: StringBuilder buf = new StringBuilder();
425: boolean inCookie = false;
426:
427: public void startElement(String uri, String localName,
428: String qName, Attributes attributes)
429: throws SAXException {
430: if (localName.equals(COOKIE_TAG.getLocalPart())
431: && uri.equals(COOKIE_TAG.getNamespaceURI()))
432: inCookie = true;
433: }
434:
435: public void characters(char ch[], int start, int length)
436: throws SAXException {
437: if (inCookie)
438: buf.append(ch, start, length);
439: }
440:
441: public void endElement(String uri, String localName,
442: String qName) throws SAXException {
443: inCookie = false;
444: }
445: }
446: CookieSniffer sniffer = new CookieSniffer();
447: epr.writeTo(new SAXResult(sniffer));
448:
449: Instance o = instances.get(sniffer.buf.toString());
450: if (o != null)
451: return o.instance;
452: return null;
453: }
454:
455: public void setFallbackInstance(T o) {
456: if (o != null)
457: prepare(o);
458: this .fallback = o;
459: }
460:
461: public void setTimeout(long milliseconds, Callback<T> callback) {
462: if (milliseconds < 0)
463: throw new IllegalArgumentException();
464: this .timeoutMilliseconds = milliseconds;
465: this .timeoutCallback = callback;
466: if (timeoutMilliseconds > 0)
467: startTimer();
468: }
469:
470: public void touch(T o) {
471: String key = reverseInstances.get(o);
472: if (key == null)
473: return; // already unexported.
474: Instance inst = instances.get(key);
475: if (inst == null)
476: return;
477: inst.restartTimer();
478: }
479:
480: /**
481: * Timer that controls the instance time out. Lazily created.
482: */
483: private static volatile Timer timer;
484:
485: private static synchronized void startTimer() {
486: if (timer == null)
487: timer = new Timer(
488: "JAX-WS stateful web service timeout timer");
489: }
490:
491: private static final QName COOKIE_TAG = new QName(
492: "http://jax-ws.dev.java.net/xml/ns/", "objectId", "jaxws");
493:
494: private static final Logger logger = Logger
495: .getLogger(com.sun.xml.ws.util.Constants.LoggingDomain
496: + ".server");
497: }
|