| /* |
| * Copyright 2005-2006 Sun Microsystems, Inc. All Rights Reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Sun designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Sun in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
| * CA 95054 USA or visit www.sun.com if you need additional information or |
| * have any questions. |
| */ |
| |
| package com.sun.xml.internal.ws.server; |
| |
| import com.sun.istack.internal.FragmentContentHandler; |
| import com.sun.istack.internal.NotNull; |
| import com.sun.istack.internal.Nullable; |
| import com.sun.xml.internal.stream.buffer.XMLStreamBufferSource; |
| import com.sun.xml.internal.stream.buffer.stax.StreamWriterBufferCreator; |
| import com.sun.xml.internal.ws.api.addressing.AddressingVersion; |
| import com.sun.xml.internal.ws.api.message.Header; |
| import com.sun.xml.internal.ws.api.message.HeaderList; |
| import com.sun.xml.internal.ws.api.message.Packet; |
| import com.sun.xml.internal.ws.api.server.InstanceResolver; |
| import com.sun.xml.internal.ws.api.server.WSEndpoint; |
| import com.sun.xml.internal.ws.api.server.WSWebServiceContext; |
| import com.sun.xml.internal.ws.developer.EPRRecipe; |
| import com.sun.xml.internal.ws.developer.StatefulWebServiceManager; |
| import com.sun.xml.internal.ws.resources.ServerMessages; |
| import com.sun.xml.internal.ws.spi.ProviderImpl; |
| import com.sun.xml.internal.ws.util.xml.ContentHandlerToXMLStreamWriter; |
| import com.sun.xml.internal.ws.util.xml.XmlUtil; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| import javax.xml.namespace.QName; |
| import javax.xml.stream.XMLStreamException; |
| import javax.xml.transform.Source; |
| import javax.xml.transform.Transformer; |
| import javax.xml.transform.TransformerException; |
| import javax.xml.transform.sax.SAXResult; |
| import javax.xml.ws.EndpointReference; |
| import javax.xml.ws.WebServiceContext; |
| import javax.xml.ws.WebServiceException; |
| import javax.xml.ws.wsaddressing.W3CEndpointReference; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| import java.util.UUID; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * {@link InstanceResolver} that looks at JAX-WS cookie header to |
| * determine the instance to which a message will be routed. |
| * |
| * <p> |
| * See {@link StatefulWebServiceManager} for more about user-level semantics. |
| * |
| * @author Kohsuke Kawaguchi |
| */ |
| public final class StatefulInstanceResolver<T> extends AbstractMultiInstanceResolver<T> implements StatefulWebServiceManager<T> { |
| /** |
| * This instance is used for serving messages that have no cookie |
| * or cookie value that the server doesn't recognize. |
| */ |
| private volatile @Nullable T fallback; |
| |
| /** |
| * Maintains the stateful service instance and its time-out timer. |
| */ |
| private final class Instance { |
| final @NotNull T instance; |
| TimerTask task; |
| |
| public Instance(T instance) { |
| this.instance = instance; |
| } |
| |
| /** |
| * Resets the timer. |
| */ |
| public synchronized void restartTimer() { |
| cancel(); |
| if(timeoutMilliseconds==0) return; // no timer |
| |
| task = new TimerTask() { |
| public void run() { |
| try { |
| Callback<T> cb = timeoutCallback; |
| if(cb!=null) { |
| cb.onTimeout(instance,StatefulInstanceResolver.this); |
| return; |
| } |
| // default operation is to unexport it. |
| unexport(instance); |
| } catch (Throwable e) { |
| // don't let an error in the code kill the timer thread |
| logger.log(Level.SEVERE, "time out handler failed", e); |
| } |
| } |
| }; |
| timer.schedule(task,timeoutMilliseconds); |
| } |
| |
| /** |
| * Cancels the timer. |
| */ |
| public synchronized void cancel() { |
| if(task!=null) |
| task.cancel(); |
| task = null; |
| } |
| } |
| |
| /** |
| * Maps object ID to instances. |
| */ |
| private final Map<String,Instance> instances = Collections.synchronizedMap(new HashMap<String,Instance>()); |
| /** |
| * Reverse look up for {@link #instances}. |
| */ |
| private final Map<T,String> reverseInstances = Collections.synchronizedMap(new HashMap<T,String>()); |
| |
| // time out control. 0=disabled |
| private volatile long timeoutMilliseconds = 0; |
| private volatile Callback<T> timeoutCallback; |
| |
| public StatefulInstanceResolver(Class<T> clazz) { |
| super(clazz); |
| } |
| |
| @Override |
| public @NotNull T resolve(Packet request) { |
| HeaderList headers = request.getMessage().getHeaders(); |
| Header header = headers.get(COOKIE_TAG, true); |
| String id=null; |
| if(header!=null) { |
| // find the instance |
| id = header.getStringContent(); |
| Instance o = instances.get(id); |
| if(o!=null) { |
| o.restartTimer(); |
| return o.instance; |
| } |
| |
| // huh? what is this ID? |
| logger.log(Level.INFO,"Request had an unrecognized object ID "+id); |
| } |
| |
| // need to fallback |
| T fallback = this.fallback; |
| if(fallback!=null) |
| return fallback; |
| |
| if(id==null) |
| throw new WebServiceException(ServerMessages.STATEFUL_COOKIE_HEADER_REQUIRED(COOKIE_TAG)); |
| else |
| throw new WebServiceException(ServerMessages.STATEFUL_COOKIE_HEADER_INCORRECT(COOKIE_TAG,id)); |
| } |
| |
| @Override |
| public void start(WSWebServiceContext wsc, WSEndpoint endpoint) { |
| super.start(wsc,endpoint); |
| |
| if(endpoint.getBinding().getAddressingVersion()==null) |
| // addressing is not enabled. |
| throw new WebServiceException(ServerMessages.STATEFUL_REQURES_ADDRESSING(clazz)); |
| |
| // inject StatefulWebServiceManager. |
| for(Field field: clazz.getDeclaredFields()) { |
| if(field.getType()==StatefulWebServiceManager.class) { |
| if(!Modifier.isStatic(field.getModifiers())) |
| throw new WebServiceException(ServerMessages.STATIC_RESOURCE_INJECTION_ONLY(StatefulWebServiceManager.class,field)); |
| new FieldInjectionPlan<T,StatefulWebServiceManager>(field).inject(null,this); |
| } |
| } |
| |
| for(Method method : clazz.getDeclaredMethods()) { |
| Class[] paramTypes = method.getParameterTypes(); |
| if (paramTypes.length != 1) |
| continue; // not what we are looking for |
| |
| if(paramTypes[0]==StatefulWebServiceManager.class) { |
| if(!Modifier.isStatic(method.getModifiers())) |
| throw new WebServiceException(ServerMessages.STATIC_RESOURCE_INJECTION_ONLY(StatefulWebServiceManager.class,method)); |
| |
| new MethodInjectionPlan<T,StatefulWebServiceManager>(method).inject(null,this); |
| } |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| reverseInstances.clear(); |
| synchronized(instances) { |
| for (Instance t : instances.values()) { |
| t.cancel(); |
| dispose(t.instance); |
| } |
| instances.clear(); |
| } |
| if(fallback!=null) |
| dispose(fallback); |
| fallback = null; |
| } |
| |
| @NotNull |
| public W3CEndpointReference export(T o) { |
| return export(W3CEndpointReference.class,o); |
| } |
| |
| @NotNull |
| public <EPR extends EndpointReference>EPR export(Class<EPR> epr, T o) { |
| return export(epr,o,null); |
| } |
| |
| public <EPR extends EndpointReference> EPR export(Class<EPR> epr, T o, EPRRecipe recipe) { |
| return export(epr, InvokerTube.getCurrentPacket(), o, recipe); |
| } |
| |
| @NotNull |
| public <EPR extends EndpointReference>EPR export(Class<EPR> epr, WebServiceContext context, T o) { |
| if (context instanceof WSWebServiceContext) { |
| WSWebServiceContext wswsc = (WSWebServiceContext) context; |
| return export(epr, wswsc.getRequestPacket(), o); |
| } |
| |
| throw new WebServiceException(ServerMessages.STATEFUL_INVALID_WEBSERVICE_CONTEXT(context)); |
| } |
| |
| @NotNull |
| public <EPR extends EndpointReference> EPR export(Class<EPR> adrsVer, @NotNull Packet currentRequest, T o) { |
| return export(adrsVer,currentRequest,o,null); |
| } |
| |
| public <EPR extends EndpointReference> EPR export(Class<EPR> adrsVer, @NotNull Packet currentRequest, T o, EPRRecipe recipe) { |
| return export(adrsVer, currentRequest.webServiceContextDelegate.getEPRAddress(currentRequest,owner), o, recipe); |
| } |
| |
| @NotNull |
| public <EPR extends EndpointReference> EPR export(Class<EPR> adrsVer, String endpointAddress, T o) { |
| return export(adrsVer,endpointAddress,o,null); |
| } |
| |
| @NotNull |
| public <EPR extends EndpointReference> EPR export(Class<EPR> adrsVer, String endpointAddress, T o, EPRRecipe recipe) { |
| if(endpointAddress==null) |
| throw new IllegalArgumentException("No address available"); |
| |
| String key = reverseInstances.get(o); |
| |
| if(key!=null) return createEPR(key, adrsVer, endpointAddress, recipe); |
| |
| // not exported yet. |
| synchronized(this) { |
| // double check now in the synchronization block to |
| // really make sure that we can export. |
| key = reverseInstances.get(o); |
| if(key!=null) return createEPR(key, adrsVer, endpointAddress, recipe); |
| |
| if(o!=null) |
| prepare(o); |
| key = UUID.randomUUID().toString(); |
| Instance instance = new Instance(o); |
| instances.put(key, instance); |
| reverseInstances.put(o,key); |
| if(timeoutMilliseconds!=0) |
| instance.restartTimer(); |
| } |
| |
| return createEPR(key, adrsVer, endpointAddress, recipe); |
| } |
| |
| /** |
| * Creates an EPR that has the right key. |
| */ |
| private <EPR extends EndpointReference> EPR createEPR(String key, Class<EPR> eprClass, String address, EPRRecipe recipe) { |
| AddressingVersion adrsVer = AddressingVersion.fromSpecClass(eprClass); |
| |
| try { |
| StreamWriterBufferCreator w = new StreamWriterBufferCreator(); |
| |
| w.writeStartDocument(); |
| w.writeStartElement("wsa","EndpointReference", adrsVer.nsUri); |
| w.writeNamespace("wsa",adrsVer.nsUri); |
| |
| w.writeStartElement("wsa","Address",adrsVer.nsUri); |
| w.writeCharacters(address); |
| w.writeEndElement(); |
| |
| w.writeStartElement("wsa","ReferenceParameters",adrsVer.nsUri); |
| w.writeStartElement(COOKIE_TAG.getPrefix(), COOKIE_TAG.getLocalPart(), COOKIE_TAG.getNamespaceURI()); |
| w.writeCharacters(key); |
| w.writeEndElement(); |
| if(recipe!=null) { |
| for (Header h : recipe.getReferenceParameters()) |
| h.writeTo(w); |
| } |
| w.writeEndElement(); |
| |
| if(recipe!=null) { |
| List<Source> metadata = recipe.getMetadata(); |
| if(!metadata.isEmpty()) { |
| w.writeStartElement("wsa","Metadata",adrsVer.nsUri); |
| Transformer t = XmlUtil.newTransformer(); |
| for (Source s : metadata) |
| try { |
| t.transform(s, |
| new SAXResult(new FragmentContentHandler(new ContentHandlerToXMLStreamWriter(w)))); |
| } catch (TransformerException e) { |
| throw new IllegalArgumentException("Unable to write EPR metadata "+s,e); |
| } |
| w.writeEndElement(); |
| } |
| } |
| |
| w.writeEndElement(); |
| w.writeEndDocument(); |
| |
| // TODO: this can be done better by writing SAX code that produces infoset |
| // and setting that as Source. |
| return eprClass.cast(ProviderImpl.INSTANCE.readEndpointReference( |
| new XMLStreamBufferSource(w.getXMLStreamBuffer()))); |
| } catch (XMLStreamException e) { |
| throw new Error(e); // this must be a bug in our code |
| } |
| } |
| |
| public void unexport(@Nullable T o) { |
| if(o==null) return; |
| String key = reverseInstances.get(o); |
| if(key==null) return; // already unexported |
| instances.remove(key); |
| } |
| |
| public T resolve(EndpointReference epr) { |
| class CookieSniffer extends DefaultHandler { |
| StringBuilder buf = new StringBuilder(); |
| boolean inCookie = false; |
| public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { |
| if(localName.equals(COOKIE_TAG.getLocalPart()) && uri.equals(COOKIE_TAG.getNamespaceURI())) |
| inCookie = true; |
| } |
| public void characters(char ch[], int start, int length) throws SAXException { |
| if(inCookie) |
| buf.append(ch,start,length); |
| } |
| public void endElement(String uri, String localName, String qName) throws SAXException { |
| inCookie = false; |
| } |
| } |
| CookieSniffer sniffer = new CookieSniffer(); |
| epr.writeTo(new SAXResult(sniffer)); |
| |
| Instance o = instances.get(sniffer.buf.toString()); |
| if(o!=null) |
| return o.instance; |
| return null; |
| } |
| |
| public void setFallbackInstance(T o) { |
| if(o!=null) |
| prepare(o); |
| this.fallback = o; |
| } |
| |
| public void setTimeout(long milliseconds, Callback<T> callback) { |
| if(milliseconds<0) |
| throw new IllegalArgumentException(); |
| this.timeoutMilliseconds = milliseconds; |
| this.timeoutCallback = callback; |
| if(timeoutMilliseconds>0) |
| startTimer(); |
| } |
| |
| public void touch(T o) { |
| String key = reverseInstances.get(o); |
| if(key==null) return; // already unexported. |
| Instance inst = instances.get(key); |
| if(inst==null) return; |
| inst.restartTimer(); |
| } |
| |
| /** |
| * Timer that controls the instance time out. Lazily created. |
| */ |
| private static volatile Timer timer; |
| |
| private static synchronized void startTimer() { |
| if(timer==null) |
| timer = new Timer("JAX-WS stateful web service timeout timer"); |
| } |
| |
| |
| private static final QName COOKIE_TAG = new QName("http://jax-ws.dev.java.net/xml/ns/","objectId","jaxws"); |
| |
| private static final Logger logger = |
| Logger.getLogger(com.sun.xml.internal.ws.util.Constants.LoggingDomain + ".server"); |
| } |