blob: 6c1fb1f2a7dec16a3b2693e29163eccf42055578 [file] [log] [blame]
/*
* 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");
}