diff --git a/src/main/java/org/cafesip/sipunit/EventSubscriber.java b/src/main/java/org/cafesip/sipunit/EventSubscriber.java index 6a1dd0ac22..71e61166e7 100644 --- a/src/main/java/org/cafesip/sipunit/EventSubscriber.java +++ b/src/main/java/org/cafesip/sipunit/EventSubscriber.java @@ -16,41 +16,19 @@ package org.cafesip.sipunit; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.EventObject; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import javax.sip.Dialog; -import javax.sip.RequestEvent; -import javax.sip.ResponseEvent; -import javax.sip.SipProvider; -import javax.sip.TimeoutEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sip.*; import javax.sip.address.Address; import javax.sip.address.AddressFactory; import javax.sip.address.SipURI; import javax.sip.address.URI; -import javax.sip.header.AcceptHeader; -import javax.sip.header.CSeqHeader; -import javax.sip.header.CallIdHeader; -import javax.sip.header.ContactHeader; -import javax.sip.header.EventHeader; -import javax.sip.header.ExpiresHeader; -import javax.sip.header.FromHeader; -import javax.sip.header.Header; -import javax.sip.header.HeaderFactory; -import javax.sip.header.MaxForwardsHeader; -import javax.sip.header.ReferToHeader; -import javax.sip.header.SubscriptionStateHeader; -import javax.sip.header.ToHeader; -import javax.sip.header.ViaHeader; +import javax.sip.header.*; import javax.sip.message.Request; import javax.sip.message.Response; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.text.ParseException; +import java.util.*; /** * The EventSubscriber class represents a generic subscription conforming to the event subscription @@ -590,7 +568,7 @@ private void processRequest(RequestEvent requestEvent) { CSeqHeader rcvSeqHdr = (CSeqHeader) request.getHeader(CSeqHeader.NAME); if (rcvSeqHdr == null) { - EventSubscriber.sendResponse(parent, requestEvent, SipResponse.BAD_REQUEST, + parent.sendResponse(requestEvent, SipResponse.BAD_REQUEST, "no CSEQ header received"); String err = "*** NOTIFY REQUEST ERROR *** (" + targetUri + ") - no CSEQ header received"; @@ -604,7 +582,7 @@ private void processRequest(RequestEvent requestEvent) { if (notifyCSeq != null) { // This is not the first NOTIFY if (rcvSeqHdr.getSeqNumber() <= notifyCSeq.getSeqNumber()) { - EventSubscriber.sendResponse(parent, requestEvent, SipResponse.OK, "OK"); + parent.sendResponse(requestEvent, SipResponse.OK, "OK"); LOG.trace("Received NOTIFY CSEQ {} not new, discarding message", rcvSeqHdr.getSeqNumber()); return; @@ -830,21 +808,13 @@ private void authorizeSubscribe(Response resp, Request msg) throws SubscriptionE } + /** + * @deprecated by instance method call of SipPhone + * @see SipPhone#sendResponse(RequestEvent, int, String) + */ + @Deprecated protected static void sendResponse(SipPhone parent, RequestEvent req, int status, String reason) { - try { - Response response = parent.getMessageFactory().createResponse(status, req.getRequest()); - response.setReasonPhrase(reason); - - if (req.getServerTransaction() != null) { - req.getServerTransaction().sendResponse(response); - return; - } - - ((SipProvider) req.getSource()).sendResponse(response); - } catch (Exception e) { - LOG.error("Failure sending error response (" + reason + ") for received " - + req.getRequest().getMethod() + ", Exception: " + e.toString(), e); - } + parent.sendResponse(req, status, reason); } /** @@ -1021,13 +991,15 @@ public boolean replyToNotify(RequestEvent reqevent, Response response) { return true; } - protected boolean messageForMe(javax.sip.message.Message msg) { - /* - * NOTIFY requests are matched to SUBSCRIBE/REFER requests if they contain the same "Call-ID", a - * "To" header "tag" parameter which matches the "From" header "tag" parameter of the - * SUBSCRIBE/REFER, and the same "Event" header field. - */ - + /** + * NOTIFY requests are matched to SUBSCRIBE/REFER requests if they contain the same "Call-ID", a + * "To" header "tag" parameter which matches the "From" header "tag" parameter of the + * SUBSCRIBE/REFER, and the same "Event" header field. + * + * @param msg The NOTIFY message whose headers will be evaluated and matched + * @return If the message matches to this event subscriber + */ + public boolean messageForMe(javax.sip.message.Message msg) { Request lastSentRequest = getLastSentRequest(); if (lastSentRequest == null) { @@ -1048,19 +1020,19 @@ protected boolean messageForMe(javax.sip.message.Message msg) { return false; } - if (hdr.getCallId().equals(sentHdr.getCallId()) == false) { + if (!hdr.getCallId().equals(sentHdr.getCallId())) { return false; } // check to-tag = from-tag, (my tag), and event header // fields same as in sent request - ToHeader tohdr = (ToHeader) msg.getHeader(ToHeader.NAME); - if (tohdr == null) { + ToHeader toHeader = (ToHeader) msg.getHeader(ToHeader.NAME); + if (toHeader == null) { return false; } - String toTag = tohdr.getTag(); + String toTag = toHeader.getTag(); if (toTag == null) { return false; } @@ -1071,30 +1043,23 @@ protected boolean messageForMe(javax.sip.message.Message msg) { } String fromTag = sentFrom.getTag(); - if (fromTag == null) { - return false; - } - - if (toTag.equals(fromTag) == false) { - return false; - } - - return eventForMe(msg, lastSentRequest); + return fromTag != null && toTag.equals(fromTag) && eventForMe(msg, lastSentRequest); } - protected boolean eventForMe(javax.sip.message.Message msg, Request lastSentRequest) { - EventHeader eventhdr = (EventHeader) msg.getHeader(EventHeader.NAME); - EventHeader sentEventhdr = (EventHeader) lastSentRequest.getHeader(EventHeader.NAME); - - if ((eventhdr == null) || (sentEventhdr == null)) { - return false; - } + /** + * @param msg The inbound NOTIFY request + * @param lastSentRequest The last sent request by an EventSubscriber + * @return If the NOTIFY request and the last sent request event headers match. + */ + public boolean eventForMe(javax.sip.message.Message msg, Request lastSentRequest) { + EventHeader eventHeader = (EventHeader) msg.getHeader(EventHeader.NAME); + EventHeader sentEventHeader = (EventHeader) lastSentRequest.getHeader(EventHeader.NAME); - if (eventhdr.equals(sentEventhdr) == false) { + if (eventHeader == null || sentEventHeader == null) { return false; } - return true; + return eventHeader.equals(sentEventHeader); } private void validateEventHeader(EventHeader receivedHdr, EventHeader sentHdr) diff --git a/src/main/java/org/cafesip/sipunit/SipPhone.java b/src/main/java/org/cafesip/sipunit/SipPhone.java index ed834a085f..0e587a4949 100644 --- a/src/main/java/org/cafesip/sipunit/SipPhone.java +++ b/src/main/java/org/cafesip/sipunit/SipPhone.java @@ -1,6 +1,6 @@ /* * Created on Feb 19, 2005 - * + * * Copyright 2005 CafeSip.org * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -17,46 +17,25 @@ package org.cafesip.sipunit; +import org.cafesip.sipunit.processing.RequestProcessingResult; +import org.cafesip.sipunit.processing.RequestProcessor; +import org.cafesip.sipunit.processing.notify.ConferenceEventStrategy; +import org.cafesip.sipunit.processing.notify.PresenceEventStrategy; +import org.cafesip.sipunit.processing.notify.ReferEventStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.EventObject; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; - -import javax.sip.Dialog; -import javax.sip.InvalidArgumentException; -import javax.sip.RequestEvent; -import javax.sip.ResponseEvent; -import javax.sip.TimeoutEvent; +import javax.sip.*; import javax.sip.address.Address; import javax.sip.address.AddressFactory; import javax.sip.address.SipURI; import javax.sip.address.URI; -import javax.sip.header.AuthorizationHeader; -import javax.sip.header.CSeqHeader; -import javax.sip.header.CallIdHeader; -import javax.sip.header.ContactHeader; -import javax.sip.header.EventHeader; -import javax.sip.header.ExpiresHeader; -import javax.sip.header.FromHeader; -import javax.sip.header.Header; -import javax.sip.header.HeaderFactory; -import javax.sip.header.MaxForwardsHeader; -import javax.sip.header.ProxyAuthenticateHeader; -import javax.sip.header.ToHeader; -import javax.sip.header.ViaHeader; -import javax.sip.header.WWWAuthenticateHeader; +import javax.sip.header.*; import javax.sip.message.MessageFactory; import javax.sip.message.Request; import javax.sip.message.Response; +import java.text.ParseException; +import java.util.*; /** * This class provides a test program with User Agent (UA) access to the SIP protocol in the form of @@ -65,10 +44,10 @@ * (SUBSCRIBE/NOTIFY) operations, call refer, etc. In future, a SipPhone object can have more than * one SipCall object associated with it but currently only one is supported. Multiple subscriptions * (buddy/presence, refer) are supported per SipPhone object. - * + * *

* A SipPhone object is created by calling SipStack.createSipPhone(). - * + * *

* Many of the methods in this class return an object or true return value if successful. In case of * an error or caller-specified timeout, a null object or a false is returned. The @@ -82,9 +61,9 @@ * indicating the cause of the problem. If an exception was involved, this string will contain the * name of the Exception class and the exception message. This class has a method, format(), which * can be called to obtain a human-readable string containing all of this error information. - * + * * @author Amit Chatterjee, Becky McElroy - * + * */ public class SipPhone extends SipSession implements SipActionObject, RequestListener { @@ -126,6 +105,16 @@ public class SipPhone extends SipSession implements SipActionObject, RequestList private List refererList = new ArrayList<>(); + /** + * Processes NOTIFY request for this user agents depending on the configured processing strategy. If no strategies + * are matched to the request being processed, then the processed NOTIFY is deemed orphaned and this UA will return + * {@link SipResponse#CALL_OR_TRANSACTION_DOES_NOT_EXIST} + */ + private RequestProcessor notifyProcessor = new RequestProcessor<>( + // Test that we support the event specified in the event header + new PresenceEventStrategy(), new ReferEventStrategy(), new ConferenceEventStrategy() + ); + protected SipPhone(SipStack stack, String host, String proto, int port, String me) throws ParseException, InvalidArgumentException { super(stack, host, proto, port, me); @@ -146,11 +135,11 @@ protected SipPhone(SipStack stack, String host, String me) * SipPhone's proxy host, or if no proxy was specified when this SipPhone was created, the * Request-URI address information is taken from this SipPhone's URI (address of record). For * other Request-URI alternatives, see the register() method that takes parameter requestUri. - * + * *

* Initially, a REGISTER message is sent without any user name and password. If the server returns * an OK, this method returns a true value. - * + * *

* If any challenge is received in response to sending the REGISTER message (response code * UNAUTHORIZED or PROXY_AUTHENTICATION_REQUIRED), the SipPhone's credentials list is checked @@ -167,31 +156,31 @@ protected SipPhone(SipStack stack, String host, String me) * user, password). Also, the authorization created for this registration is not saved for re-use * on a later registration. IE, the user/password parameters are for a one-time, single-shot use * only. - * + * *

* After responding to the challenge(s) by resending the REGISTER message, this method returns a * true or false value depending on the outcome as indicated by the server. - * + * *

* If the contact parameter is null, user@hostname is used where hostname is the SipStack's IP * address property which defaults to InetAddress.getLocalHost().getHostAddress(), and other * SipStack properties also apply. Otherwise, the contact parameter given is used in the * Registration message sent to the server. - * + * *

* If the expiry parameter is 0, the registration request never expires. Otherwise, the duration, * given in seconds, is sent to server. - * + * *

* This method can be called repeatedly to update the expiry or to add new contacts. - * + * *

* This method determines the contact information for this user agent, whether the registration * was successful or not. If successful, the contact information may have been updated by the * server (such as the expiry time, if not specified to this method by the caller). Once this * method has been called, the test program can get information about the contact for this agent * by calling the *MyContact*() getter methods. - * + * * @param user Optional - user name for authenticating with the server. Required if the server * issues an authentication challenge. * @param password Optional - used only if the server issues an authentication challenge. @@ -231,7 +220,7 @@ public boolean register(String user, String password, String contact, int expiry * is derived from this SipPhone's URI, or address of record (for example, if this SipPhone's * address of record is "sip:amit@cafesip.org", the REGISTER Request-URI will be sip:cafesip.org). * Otherwise, the requestUri passed in is used for the REGISTER Request-URI. - * + * */ public boolean register(SipURI requestUri, String user, String password, String contact, int expiry, long timeout) { @@ -349,7 +338,7 @@ public boolean register(SipURI requestUri, String user, String password, String * This method is equivalent to the register(String user, String password, String contact, int * expiry, long timeout) method except with no authorization parameters specified. Call this * method if no authorization will be needed or after setting up the SipPhone's credentials list. - * + * * @param contact An URI string (ex: sip:bob@192.0.2.4) * @param expiry Expiry time in seconds, or 0 if no expiry. * @return false if registration fails or an error is encountered, true otherwise. @@ -363,16 +352,16 @@ public boolean register(String contact, int expiry) { * successful or no unregistration was needed, and false otherwise. Any authorization headers * required for the last registration are cleared out. If there was no previous registration, this * method does not send any messages. - * + * *

* If the contact parameter is null, user@hostname is unregistered where hostname is obtained by * calling InetAddr.getLocalHost(). Otherwise, the contact parameter value is used in the * unregistration message sent to the server. - * + * * @param contact The contact URI (ex: sip:bob@192.0.2.4) to unregister or "*". * @param timeout The maximum amount of time to wait for a response, in milliseconds. Use a value * of 0 to wait indefinitely. - * + * * @return true if the unregistration succeeded or no unregistration was needed, false otherwise. */ public boolean unregister(String contact, long timeout) { @@ -534,13 +523,13 @@ private Response sendRegistrationMessage(Request msg, String user, String passwo * This method is public for test purposes and for use when using low level SipSession methods for * sending/receiving messages. A test program using high level SipUnit doesn't need to call this * method. - * + * *

* This method modifies the given request to include the authorization header(s) required by the * given response. It may cache in SipPhone's authorizations list the AuthorizationHeader(s) * created here for use later. The modified Request object is returned, or null in case of error * or unresolved challenge. - * + * *

* For each received challenge present in the response message: SipPhone's credentials list is * checked first, for the realm entry. If it is not found there, the username parameter passed @@ -550,7 +539,7 @@ private Response sendRegistrationMessage(Request msg, String user, String passwo * and the authorization created here is NOT saved for later re-use. If the credentials list * contains an entry for the challenging realm, then the authorization created here is saved in * the authorizations list for later re-use. - * + * * @param response the challenge that was received * @param req_msg the request originally sent (that was challenged) * @param username see above @@ -662,19 +651,19 @@ public Request processAuthChallenge(Response response, Request req_msg, String u /* * here's replace code - * + * * ListIterator msg_headers; if (authorization instanceof ProxyAuthorizationHeader) { * msg_headers = msg.getHeaders(ProxyAuthorizationHeader.NAME); } else { msg_headers = * msg.getHeaders(AuthorizationHeader.NAME); } - * + * * boolean replaced = false; - * - * + * + * * while (msg_headers.hasNext()) { AuthorizationHeader msg_hdr = (AuthorizationHeader) * msg_headers .next(); if (msg_hdr.getRealm().equals(realm)) { * msg_headers.set(authorization); replaced = true; break; } } - * - * + * + * * if (replaced == false) { msg.addHeader(authorization); // how to bubble auth up - // * check 1.2 API } */ @@ -713,7 +702,7 @@ public Request processAuthChallenge(Response response, Request req_msg) { * only one SipCall object is supported per SipPhone. In future, when more than one SipCall per * SipPhone is supported, this method can be called multiple times to create multiple call legs on * the same SipPhone object. - * + * * @return A SipCall object unless an error is encountered. */ public SipCall createSipCall() { @@ -735,19 +724,19 @@ protected void dropCall(SipCall call) { * INVITE response status code is received. The object returned is a SipCall object representing * the outgoing call leg; that is, the UAC originating a call to the network. Then you can take * subsequent action on the call by making method calls on the SipCall object. - * + * *

* Use this method when (1) you want to establish a call without worrying about the details and * (2) your test program doesn't need to do anything else (ie, it can be blocked) until the * response code parameter passed to this method is received from the network. - * + * *

* In case the first condition above is false: If you need to see the (intermediate/provisional) * response messages as they come in, then use SipPhone.createSipCall() and * SipCall.initiateOutgoingCall() instead of this method. If your test program can tolerate being * blocked until the desired response is received, you can still use this method and later look * back at all the received responses by calling SipCall.getAllReceivedResponses(). - * + * *

* In case the second condition above is false: If your test code is handling both sides of the * call, or it has to do other things while this call establishment is in progress, then this @@ -755,8 +744,8 @@ protected void dropCall(SipCall call) { * returns a SipCall object after the INVITE has been successfully sent. Then, later on you can * check back with the SipCall object to see the call progress or block on the call establishment, * at a more convenient time. - * - * + * + * * @param to The URI string (ex: sip:bob@nist.gov) to which the call should be directed * @param response The SipResponse status code to look for after sending the INVITE. This method * returns when that status code is received. @@ -777,10 +766,10 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * This method is the same as the basic blocking makeCall() method except that it allows the * caller to specify a message body and/or additional message headers to add to or replace in the * outbound message without requiring knowledge of the JAIN-SIP API. - * + * *

* The extra parameters supported by this method are: - * + * * @param body A String to be used as the body of the message. Parameters contentType, * contentSubType must both be non-null to get the body included in the message. Use null * for no body bytes. @@ -806,7 +795,7 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * occur if your headers are not syntactically correct or contain nonsensical values (the * message may not pass through the local SIP stack). Use null for no replacement of * message headers. - * + * */ public SipCall makeCall(String to, int response, long timeout, String viaNonProxyRoute, String body, String contentType, String contentSubType, ArrayList additionalHeaders, @@ -827,10 +816,10 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * caller to specify a message body and/or additional JAIN-SIP API message headers to add to or * replace in the outbound INVITE message. Use of this method requires knowledge of the JAIN-SIP * API. - * + * *

* The extra parameters supported by this method are: - * + * * @param additionalHeaders ArrayList of javax.sip.header.Header, each element a SIP header to add * to the outbound message. These headers are added to the message after a correct message * has been constructed. Note that if you try to add a header that there is only supposed @@ -924,7 +913,7 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * etc.) are automatically collected and any received authentication challenges are automatically * handled as well. The object returned by this method is a SipCall object representing the * outgoing call leg; that is, the UAC originating a call to the network. - * + * *

* After calling this method, you can later call one or more of the following methods on the * returned SipCall object to see what happened (each is nonblocking unless otherwise noted): @@ -937,20 +926,20 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * waitOutgoingCallResponse() - BLOCKING - when your test program is done with its tasks and can * be blocked until the next response is received (if you are interested in something other than * OK) - use this only if you know that the INVITE transaction is still up. - * + * *

* Call this method when (1) you want to establish a call without worrying about the details and * (2) your test program needs to do other tasks after the INVITE is sent but before a * final/expected response is received (ie, the calling program cannot be blocked during call * establishment). - * + * *

* Otherwise: If you need to see or act on any of the (intermediate/provisional) response messages * as they come in, use SipPhone.createSipCall() and SipCall.initiateOutgoingCall() instead of * this method. If your test program doesn't need to do anything else until the call is * established: use the other SipPhone.makeCall() method which conveniently blocks until the * response code you specify is received from the network. - * + * * @param to The URI string (ex: sip:bob@nist.gov) to which the call should be directed * @param viaNonProxyRoute Indicates whether to route the INVITE via Proxy or some other route. If * null, route the call to the Proxy that was specified when the SipPhone object was @@ -958,7 +947,7 @@ public SipCall makeCall(String to, int response, long timeout, String viaNonProx * as "hostaddress:port/transport" i.e. 129.1.22.333:5060/UDP. * @return A SipCall object representing the outgoing call leg, or null if an error was * encountered. - * + * */ public SipCall makeCall(String to, String viaNonProxyRoute) { return makeCall(to, viaNonProxyRoute, null, null, null); @@ -969,10 +958,10 @@ public SipCall makeCall(String to, String viaNonProxyRoute) { * caller to specify a message body and/or additional JAIN-SIP API message headers to add to or * replace in the outbound INVITE message. Use of this method requires knowledge of the JAIN-SIP * API. - * + * *

* The extra parameters supported by this method are: - * + * * @param additionalHeaders ArrayList of javax.sip.header.Header, each element a SIP header to add * to the outbound message. These headers are added to the message after a correct message * has been constructed. Note that if you try to add a header that there is only supposed @@ -1009,9 +998,9 @@ public SipCall makeCall(String to, String viaNonProxyRoute, ArrayList

ad * This method is the same as the basic nonblocking makeCall() method except that it allows the * caller to specify a message body and/or additional message headers to add to or replace in the * outbound message without requiring knowledge of the JAIN-SIP API. - * + * * The extra parameters supported by this method are: - * + * * @param body A String to be used as the body of the message. Parameters contentType, * contentSubType must both be non-null to get the body included in the message. Use null * for no body bytes. @@ -1037,7 +1026,7 @@ public SipCall makeCall(String to, String viaNonProxyRoute, ArrayList
ad * occur if your headers are not syntactically correct or contain nonsensical values (the * message may not pass through the local SIP stack). Use null for no replacement of * message headers. - * + * */ public SipCall makeCall(String to, String viaNonProxyRoute, String body, String contentType, String contentSubType, ArrayList additionalHeaders, @@ -1058,7 +1047,7 @@ public SipCall makeCall(String to, String viaNonProxyRoute, String body, String * nor its SipSession base class should be used again after calling the dispose() method. * Server/proxy unregistration occurs and SipCall(s) associated with this SipPhone are dropped. No * un-SUBSCRIBE is done for active Subscriptions in the buddy list. - * + * * @see org.cafesip.sipunit.SipCall#dispose() */ public void dispose() { @@ -1106,7 +1095,7 @@ protected CallIdHeader getNewCallIdHeader() * agent. This may be the value associated with the last registration attempt or as defaulted to * user@host if no registration has occurred. Or, if the setPublicAddress() has been called on * this object, the returned value will reflect the most recent call to setPublicAddress(). - * + * * @return The SipContact object currently in effect for this user agent */ public SipContact getContactInfo() { @@ -1115,12 +1104,12 @@ public SipContact getContactInfo() { /** * This method is the same as getContactInfo(). - * + * * @deprecated Use getContactInfo() instead of this method, the term 'local' in the method name is * misleading if the SipUnit test is running behind a NAT. - * + * * @return The SipContact object currently in effect for this user agent - * + * */ public SipContact getLocalContactInfo() { return getContactInfo(); @@ -1136,7 +1125,7 @@ protected void updateContactInfo(ContactHeader hdr) { /** * Gets the user Address for this SipPhone. This is the same address used in the * "from" header field. - * + * * @return Returns the javax.sip.address.Address for this SipPhone (UA). */ public Address getAddress() { @@ -1145,7 +1134,7 @@ public Address getAddress() { /** * Gets the request sent at the last successful registration. - * + * * @return Returns the lastRegistrationRequest. */ protected Request getLastRegistrationRequest() { @@ -1183,7 +1172,7 @@ protected void addAuthorizations(String call_id, Request msg) { /** * This method adds a new credential to the credentials list or updates an existing credential in * the list. - * + * * @param c the credential to be added/updated. */ public void addUpdateCredential(Credential c) { @@ -1192,7 +1181,7 @@ public void addUpdateCredential(Credential c) { /** * This method removes a credential from the credentials list. - * + * * @param c the credential to be removed. */ public void removeCredential(Credential c) { @@ -1201,7 +1190,7 @@ public void removeCredential(Credential c) { /** * This method removes a credential from the credentials list. - * + * * @param realm the realm associated with the credential to be removed. */ public void removeCredential(String realm) { @@ -1215,7 +1204,8 @@ public void clearCredentials() { credentials.clear(); } - /* + /** + * @param event Event received. * @see org.cafesip.sipunit.RequestListener#processEvent(java.util.EventObject) */ public void processEvent(EventObject event) { @@ -1228,97 +1218,131 @@ public void processEvent(EventObject event) { } private void processRequestEvent(RequestEvent requestEvent) { + boolean validationSuccessful = isNotifyRequest(requestEvent) && containsEventHeader(requestEvent); + RequestProcessingResult result = new RequestProcessingResult(!validationSuccessful, false); + + // Validation passed, continue with event handling + if (!result.isProcessed()) { + result = notifyProcessor.processRequestEvent(requestEvent, this); + } + + if (!result.isProcessed()) { + handleUnknownEvent(requestEvent); + } else if (!result.isSuccessful()) { + handleOrphanedEvent(requestEvent); + } + } + + private boolean isNotifyRequest(RequestEvent requestEvent) { Request request = requestEvent.getRequest(); - Dialog d = requestEvent.getDialog(); - if (request.getMethod().equals(Request.NOTIFY) == false) { - EventSubscriber.sendResponse(this, requestEvent, SipResponse.SERVER_INTERNAL_ERROR, - "Expected to receive a NOTIFY request, but instead got: " + request.getMethod()); + if (!request.getMethod().equals(Request.NOTIFY)) { + this.sendResponse(requestEvent, SipResponse.SERVER_INTERNAL_ERROR, + "Expected to receive a NOTIFY request, but instead got: " + request.getMethod()); String err = "*** NOTIFY REQUEST ERROR *** (SipPhone " + me - + ") - SipPhone.processRequestEvent() - incoming request was misrouted, expected NOTIFY but got " - + request.getMethod() + " : " + request; - distributeEventError(err); + + ") - SipPhone.processRequestEvent() - incoming request was routed incorrectly, expected NOTIFY but got " + + request.getMethod() + " : " + requestEvent; + this.distributeEventError(err); LOG.error(err); - return; + return false; } - // find the EventSubscriber that this message is for - get the - // subscription target uri from the message + return true; + } - FromHeader from = (FromHeader) request.getHeader(FromHeader.NAME); + private boolean containsEventHeader(RequestEvent requestEvent) { + Request request = requestEvent.getRequest(); EventHeader event = (EventHeader) request.getHeader(EventHeader.NAME); if (event == null) { - EventSubscriber.sendResponse(this, requestEvent, SipResponse.BAD_REQUEST, - "Received a NOTIFY request with no event header"); + this.sendResponse(requestEvent, SipResponse.BAD_REQUEST, + "Received a NOTIFY request with no event header"); String err = "*** NOTIFY REQUEST ERROR *** (SipPhone " + me - + ") - SipPhone.processRequestEvent() - Received a NOTIFY request with no event header : \n" - + request; - distributeEventError(err); + + ") - SipPhone.processRequestEvent() - Received a NOTIFY request with no event header : \n" + + request; + this.distributeEventError(err); LOG.error(err); - return; + return false; } - if (event.getEventType().equals("presence")) { - PresenceSubscriber subs = getBuddyInfo(from.getAddress().getURI().toString()); - if (subs != null) { - if (subs.messageForMe(request) == true) { - subs.processEvent(requestEvent); - return; - } - } - } else if (event.getEventType().equals("refer")) { - if (d != null) { - List refers = getRefererInfoByDialog(d.getDialogId()); - for (ReferSubscriber s : refers) { - if (s.messageForMe(request) == true) { - s.processEvent(requestEvent); - return; - } - } - } - } else if (event.getEventType().equals("conference")) { - // just return so that the test can use waitRequest() - return; - } else { - String error = - "Received a NOTIFY request with unrecognized event header : " + event.getEventType(); - EventSubscriber.sendResponse(this, requestEvent, SipResponse.BAD_EVENT, error); + return true; + } + + private void handleUnknownEvent(RequestEvent requestEvent) { + Request request = requestEvent.getRequest(); + EventHeader event = (EventHeader) request.getHeader(EventHeader.NAME); + + if (event != null) { + String error = "Received a NOTIFY request with unrecognized event header : " + event.getEventType(); + this.sendResponse(requestEvent, SipResponse.BAD_EVENT, error); String err = "*** NOTIFY REQUEST ERROR *** (SipPhone " + me - + ") - SipPhone.processRequestEvent() - " + error + " : \n" + request; - distributeEventError(err); + + ") - SipPhone.processRequestEvent() - " + error + " : \n" + request; + this.distributeEventError(err); LOG.error(err); - return; } + } + + /** + * We need to enforce a default handling method. Assuming there are no strategies left, we need to respond to the + * case where we aren't able to do any NOTIFY processing + */ + private void handleOrphanedEvent(RequestEvent requestEvent) { + Request request = requestEvent.getRequest(); + FromHeader from = (FromHeader) request.getHeader(FromHeader.NAME); // no Subscription match for this NOTIFY - 481 status String err = "Received orphan NOTIFY message (no matching subscription) from " - + from.getAddress().getURI().toString(); + + from.getAddress().getURI().toString(); - EventSubscriber.sendResponse(this, requestEvent, SipResponse.CALL_OR_TRANSACTION_DOES_NOT_EXIST, - err); + this.sendResponse(requestEvent, SipResponse.CALL_OR_TRANSACTION_DOES_NOT_EXIST, err); String error = "*** NOTIFY REQUEST ERROR *** (" + from.getAddress().getURI().toString() - + ") : " + err + " : " + request.toString(); - distributeEventError(error); + + ") : " + err + " : " + request.toString(); + this.distributeEventError(error); LOG.error(error); + } - return; + /** + * Common-base response sending function used in NOTIFY processing + * + * @param req The request this response is being constructed for + * @param status Status code + * @param reason Reason string displayed along the status code + */ + protected void sendResponse(RequestEvent req, int status, String reason) { + try { + Response response = getMessageFactory().createResponse(status, req.getRequest()); + response.setReasonPhrase(reason); + + if (req.getServerTransaction() != null) { + req.getServerTransaction().sendResponse(response); + return; + } + + ((SipProvider) req.getSource()).sendResponse(response); + } catch (Exception e) { + LOG.error("Failure sending error response (" + reason + ") for received " + + req.getRequest().getMethod() + ", Exception: " + e.toString(), e); + } } - private void distributeEventError(String err) - // to all the Subscriptions - test program will need to see the error - { - List subscriptions = - new ArrayList(getBuddyList().values()); + /** + * Sets the error state for the available subscriptions of the specified phone. The error is propagated + * to all the subscriptions - test program will need to see the error. + * + * @param err The error message + * @see SipAssert#assertNoSubscriptionErrors + */ + private void distributeEventError(String err) { + List subscriptions = new ArrayList(getBuddyList().values()); subscriptions.addAll(new ArrayList<>(getRetiredBuddies().values())); subscriptions.addAll(getRefererList()); - for (EventSubscriber s : subscriptions) { - s.addEventError(err); + for (EventSubscriber eventSubscriber : subscriptions) { + eventSubscriber.addEventError(err); } } @@ -1327,17 +1351,17 @@ private void distributeEventError(String err) * tracking the buddy's presence information. Please read the SipUnit User Guide webpage Event * Subscription (at least the operation overview part) for information on how to use SipUnit * presence capabilities. - * + * *

* This method creates a SUBSCRIBE request message, sends it out, and waits for a response to be * received. It saves the received response and checks for a "proceedable" (positive) status code * value. Positive response status codes include any of the following: provisional (status / 100 * == 1), UNAUTHORIZED, PROXY_AUTHENTICATION_REQUIRED, OK and ACCEPTED. Any other status code, or * a response timeout or any other error, is considered fatal to the subscription. - * + * *

* This method blocks until one of the above outcomes is reached. - * + * *

* In the case of a positive response status code, this method returns a PresenceSubscriber object * that will represent the buddy for the life of the subscription and puts the PresenceSubscriber @@ -1348,14 +1372,14 @@ private void distributeEventError(String err) * details at any given time such as the subscription state, amount of time left on the * subscription, termination reason, presence information, details of received responses and * requests, etc. - * + * *

* In the case of a positive response status code (a non-null object is returned), you may find * out more about the response that was just received by calling the PresenceSubscriber methods * getReturnCode() and getCurrentResponse()/getLastReceivedResponse(). Your next step at this * point will be to call the PresenceSubscriber's processResponse() method to proceed with the * SUBSCRIBE processing. - * + * *

* In the case of a fatal outcome, no subscription object is created and null is returned. In this * case, call the usual SipUnit failed-operation methods to find out what happened (ie, call this @@ -1363,7 +1387,7 @@ private void distributeEventError(String err) * getReturnCode() method will tell you the response status code that was received from the * network (unless it is an internal SipUnit error code, see the SipSession javadoc for more on * that). - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the buddy to be added to the list. * @param duration the duration in seconds to put in the SUBSCRIBE message. If 0, this is * equivalent to a fetch except that the buddy stays in the buddy list even though the @@ -1425,7 +1449,7 @@ public PresenceSubscriber addBuddy(String uri, int duration, String eventId, lon * This method is the same as addBuddy(uri, duration, eventId, timeout) except that the duration * is defaulted to the default period defined in the event package RFC (3600 seconds) and no event * "id" parameter will be included. - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the buddy to be added to the list. * @param timeout The maximum amount of time to wait for a SUBSCRIBE response, in milliseconds. * Use a value of 0 to wait indefinitely. @@ -1439,7 +1463,7 @@ public PresenceSubscriber addBuddy(String uri, long timeout) { /** * This method is the same as addBuddy(uri, duration, eventId, timeout) except that no event "id" * parameter will be included. - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the buddy to be added to the list. * @param duration the duration in seconds to put in the SUBSCRIBE message. If 0, this is * equivalent to a fetch except that the buddy stays in the buddy list even though the @@ -1456,7 +1480,7 @@ public PresenceSubscriber addBuddy(String uri, int duration, long timeout) { /** * This method is the same as addBuddy(uri, duration, eventId, timeout) except that the duration * is defaulted to the default period defined in the event package RFC (3600 seconds). - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the buddy to be added to the list. * @param eventId the event "id" to use in the SUBSCRIBE message, or null for no event "id" * parameter. See addBuddy(uri, duration, eventId, timeout) javadoc for details on event @@ -1475,7 +1499,7 @@ public PresenceSubscriber addBuddy(String uri, String eventId, long timeout) { * user's presence status and information. Please read the SipUnit User Guide webpage Event * Subscription (at least the operation overview part) for information on how to use SipUnit * presence capabilities. - * + * *

* This method creates a SUBSCRIBE request message with expiry time of 0, sends it out, and waits * for a response to be received. It saves the received response and checks for a "proceedable" @@ -1483,10 +1507,10 @@ public PresenceSubscriber addBuddy(String uri, String eventId, long timeout) { * provisional (status / 100 == 1), UNAUTHORIZED, PROXY_AUTHENTICATION_REQUIRED, OK and ACCEPTED. * Any other status code, or a response timeout or any other error, is considered fatal to the * operation. - * + * *

* This method blocks until one of the above outcomes is reached. - * + * *

* In the case of a positive response status code, this method returns a PresenceSubscriber object * representing the user and puts the object in this SipPhone's retired buddy list. The retired @@ -1496,14 +1520,14 @@ public PresenceSubscriber addBuddy(String uri, String eventId, long timeout) { * PresenceSubscriber object to proceed through the remainder of the SUBSCRIBE-NOTIFY sequence and * to find out details such as the subscription state, termination reason, presence information, * details of received responses and requests, etc. - * + * *

* In the case of a positive response status code (a non-null object is returned), you may find * out more about the response that was just received by calling the PresenceSubscriber methods * getReturnCode() and getCurrentResponse()/getLastReceivedResponse(). Your next step at this * point will be to call the PresenceSubscriber's processResponse() method to proceed with the * SUBSCRIBE processing. - * + * *

* In the case of a fatal outcome, no Subscription object is created and null is returned. In this * case, call the usual SipUnit failed-operation methods to find out what happened (ie, call this @@ -1511,7 +1535,7 @@ public PresenceSubscriber addBuddy(String uri, String eventId, long timeout) { * getReturnCode() method will tell you the response status code that was received from the * network (unless it is an internal SipUnit error code, see the SipSession javadoc for more on * that). - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the user whose presence info is to be fetched. * @param eventId the event "id" to use in the SUBSCRIBE message, or null for no event "id" * parameter. Whatever is indicated here will be used subsequently, for error checking the @@ -1573,7 +1597,7 @@ public PresenceSubscriber fetchPresenceInfo(String uri, String eventId, long tim * This method is the same as fetchPresenceInfo(uri, eventId, timeout) except that no event "id" * parameter will be included in the SUBSCRIBE message. When error checking the SUBSCRIBE response * and NOTIFY from the server, no event "id" parameter will be expected. - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the user whose presence info is to be fetched. * @param timeout The maximum amount of time to wait for a SUBSCRIBE response, in milliseconds. * Use a value of 0 to wait indefinitely. @@ -1592,13 +1616,13 @@ public PresenceSubscriber fetchPresenceInfo(String uri, long timeout) { * from the returned object. The user may have been a buddy in the buddy list (but was removed * from the list by the test program), or fetchPresenceInfo() was previously called for the user * to get a one-time status report, or the user may still be in the buddy list. - * + * * @param uri the URI (ie, sip:bob@nist.gov) of the user whose subscription object is to be * returned. * @return A PresenceSubscriber object that contains information about the user's last obtained * presence status and other info, or null if there was never any status fetch done for * this user and this user was never in the buddy list. - * + * */ public PresenceSubscriber getBuddyInfo(String uri) { synchronized (buddyList) { @@ -1618,10 +1642,10 @@ public PresenceSubscriber getBuddyInfo(String uri) { * are still in the buddy list. A given buddy, or subscription, in this list may be active or not * - ie, subscription termination by the far end does not remove a buddy from this list. Buddies * are removed from the list only by the test program (by calling removeBuddy()). - * + * *

* See related methods getBuddyInfo(), getRetiredBuddies(). - * + * * @return a Hashtable of zero or more entries, where the key = URI of the buddy, value = * PresenceSubscriber object. */ @@ -1635,10 +1659,10 @@ public Map getBuddyList() { * test program removes a buddy from the buddy list. The main purpose of this list is so the last * known presence status of a user can be obtained anytime. This is required to make the fetch * case useful. - * + * *

* See related methods getBuddyInfo(), getBuddyList(). - * + * * @return a Hashtable of zero or more entries, where the key = URI of the user, value = * PresenceSubscriber object. */ @@ -1672,12 +1696,12 @@ protected boolean removeRefer(ReferSubscriber ref) { * Gets the subscription object(s) associated with the given referToUri. The returned object(s) * contains subscription state, received requests (NOTIFY's) and REFER/SUBSCRIBE responses, etc. * for outbound REFER subscription(s) associated with the referToUri. - * + * * @param referToUri the referToUri that was previously passed in to SipPhone.refer(). See javadoc * there. * @return ReferSubscriber object(s) associated with the referToUri, or an empty list if there was * never a call to SipPhone.refer() with that referToUri. - * + * */ public List getRefererInfo(SipURI referToUri) { List list = new ArrayList<>(); @@ -1697,11 +1721,11 @@ public List getRefererInfo(SipURI referToUri) { * Gets the subscription object(s) associated with the given dialog ID. The * returned object(s) contains subscription state, received requests (NOTIFY's) and * REFER/SUBSCRIBE responses, etc. for outbound REFER subscription(s) associated with the dialog. - * + * * @param dialogId the dialog ID of interest * @return ReferSubscriber object(s) associated with the dialog, or an empty list if there was * never a refer subscription associated with that dialog. - * + * */ public List getRefererInfoByDialog(String dialogId) { List list = new ArrayList<>(); @@ -1723,10 +1747,10 @@ public List getRefererInfoByDialog(String dialogId) { * lifetime of this SipPhone object. A given subscription in the list may be active or not - * subscription termination does not automatically remove a subscription from this list (calling * ReferSubscriber.dispose() does that). - * + * *

* See related method getRefererInfo(). - * + * * @return a list of ReferSubscriber objects or an empty list if there are none. */ public List getRefererList() { @@ -1735,7 +1759,7 @@ public List getRefererList() { /** * Creates an URI object useful for passing into methods such as SipPhone.refer(). - * + * * @param scheme "sip:" or "sips:" or null if the scheme is already included in the userHostPort * parameter. * @param userHostPort Addressing information in the form of: user@host:port. Port is not @@ -1813,10 +1837,10 @@ public SipURI getUri(String scheme, String userHostPort, String transportUriPara * following: provisional (status / 100 == 1), UNAUTHORIZED, PROXY_AUTHENTICATION_REQUIRED, OK and * ACCEPTED. Any other status code, or a response timeout or any other error, is considered fatal * to the subscription. - * + * *

* This method blocks until one of the above outcomes is reached. - * + * *

* In the case of a positive response status code, this method returns a ReferSubscriber object * that represents the implicit subscription. You can save the returned object yourself or @@ -1825,13 +1849,13 @@ public SipURI getUri(String scheme, String userHostPort, String transportUriPara * REFER-NOTIFY sequence as well as for future SUBSCRIBE-NOTIFY sequences on this subscription and * also to find out details at any given time such as the subscription state, amount of time left * on the subscription, termination reason, details of received responses and requests, etc. - * + * *

* In the case of a positive response status code (a non-null object is returned), you may find * out more about the response that was just received by calling the ReferSubscriber methods * getReturnCode() and getCurrentResponse(). Your next step will then be to call the * ReferSubscriber's processResponse() method to proceed with the REFER processing. - * + * *

* In the case of a fatal outcome, no subscription object is created and null is returned. In this * case, call the usual SipUnit failed-operation methods to find out what happened (ie, call this @@ -1839,7 +1863,7 @@ public SipURI getUri(String scheme, String userHostPort, String transportUriPara * getReturnCode() method will tell you the response status code that was received from the * network (unless it is an internal SipUnit error code, see the SipSession javadoc for more on * that). - * + * * @param refereeUri The URI (ie, sip:bob@nist.gov) of the far end of the subscription. This is * the party the REFER request is sent to. * @param referToUri The URI that the refereeUri is to refer to. You can use SipPhone.getUri() to @@ -1860,7 +1884,7 @@ public SipURI getUri(String scheme, String userHostPort, String transportUriPara * viaNonProxyRoute node which is specified as "hostaddress:port/transport" i.e. * 129.1.22.333:5060/UDP. A route header will be added to the REFER for this, before the * request is sent. - * + * * @return ReferSubscriber object representing the implicit subscription if the operation is * successful so far, null otherwise. */ @@ -1873,10 +1897,10 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * This method is the same as the basic out-of-dialog refer() method except that it allows the * caller to specify a message body and/or additional JAIN-SIP API message headers to add to or * replace in the outbound message. Use of this method requires knowledge of the JAIN-SIP API. - * + * *

* The extra parameters supported by this method are: - * + * * @param additionalHeaders ArrayList of javax.sip.header.Header, each element a SIP header to add * to the outbound message. These headers are added to the message after a correct message * has been constructed. Note that if you try to add a header that there is only supposed @@ -1943,10 +1967,10 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * This method is the same as the basic out-of-dialog refer() method except that it allows the * caller to specify a message body and/or additional message headers to add to or replace in the * outbound message without requiring knowledge of the JAIN-SIP API. - * + * *

* The extra parameters supported by this method are: - * + * * @param body A String to be used as the body of the message. Parameters contentType, * contentSubType must both be non-null to get the body included in the message. Use null * for no body bytes. @@ -1972,7 +1996,7 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * occur if your headers are not syntactically correct or contain nonsensical values (the * message may not pass through the local SIP stack). Use null for no replacement of * message headers. - * + * */ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventId, long timeout, String viaNonProxyRoute, String body, String contentType, String contentSubType, @@ -1995,10 +2019,10 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * provisional (status / 100 == 1), UNAUTHORIZED, PROXY_AUTHENTICATION_REQUIRED, OK and ACCEPTED. * Any other status code, or a response timeout or any other error, is considered fatal to the * subscription. - * + * *

* This method blocks until one of the above outcomes is reached. - * + * *

* In the case of a positive response status code, this method returns a ReferSubscriber object * that represents the implicit subscription. You can save the returned object yourself or @@ -2007,13 +2031,13 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * REFER-NOTIFY sequence as well as for future SUBSCRIBE-NOTIFY sequences on this subscription and * also to find out details at any given time such as the subscription state, amount of time left * on the subscription, termination reason, details of received responses and requests, etc. - * + * *

* In the case of a positive response status code (a non-null object is returned), you may find * out more about the response that was just received by calling the ReferSubscriber methods * getReturnCode() and getCurrentResponse(). Your next step will then be to call the * ReferSubscriber's processResponse() method to proceed with the REFER processing. - * + * *

* In the case of a fatal outcome, no subscription object is created and null is returned. In this * case, call the usual SipUnit failed-operation methods to find out what happened (ie, call this @@ -2021,7 +2045,7 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * getReturnCode() method will tell you the response status code that was received from the * network (unless it is an internal SipUnit error code, see the SipSession javadoc for more on * that). - * + * * @param dialog The existing dialog that this REFER should be associated with. You can get it * from SipCall.getDialog() or ReferSubscriber.getDialog(). * @param referToUri The URI that the far end of the dialog is to refer to. You can use @@ -2034,7 +2058,7 @@ public ReferSubscriber refer(String refereeUri, SipURI referToUri, String eventI * refresh() or unsubscribe(). * @param timeout The maximum amount of time to wait for a response, in milliseconds. Use a value * of 0 to wait indefinitely. - * + * * @return ReferSubscriber object representing the implicit subscription if the operation is * successful so far, null otherwise. */ @@ -2046,10 +2070,10 @@ public ReferSubscriber refer(Dialog dialog, SipURI referToUri, String eventId, l * This method is the same as the basic in-dialog refer() method except that it allows the caller * to specify a message body and/or additional JAIN-SIP API message headers to add to or replace * in the outbound message. Use of this method requires knowledge of the JAIN-SIP API. - * + * *

* The extra parameters supported by this method are: - * + * * @param additionalHeaders ArrayList of javax.sip.header.Header, each element a SIP header to add * to the outbound message. These headers are added to the message after a correct message * has been constructed. Note that if you try to add a header that there is only supposed @@ -2114,10 +2138,10 @@ public ReferSubscriber refer(Dialog dialog, SipURI referToUri, String eventId, l * This method is the same as the basic in-dialog refer() method except that it allows the caller * to specify a message body and/or additional message headers to add to or replace in the * outbound message without requiring knowledge of the JAIN-SIP API. - * + * *

* The extra parameters supported by this method are: - * + * * @param body A String to be used as the body of the message. Parameters contentType, * contentSubType must both be non-null to get the body included in the message. Use null * for no body bytes. @@ -2143,7 +2167,7 @@ public ReferSubscriber refer(Dialog dialog, SipURI referToUri, String eventId, l * occur if your headers are not syntactically correct or contain nonsensical values (the * message may not pass through the local SIP stack). Use null for no replacement of * message headers. - * + * */ public ReferSubscriber refer(Dialog dialog, SipURI referToUri, String eventId, long timeout, String body, String contentType, String contentSubType, ArrayList additionalHeaders, @@ -2184,4 +2208,12 @@ protected ContactHeader updateContactInfo(String contact, String displayName) th throw new Exception("Update contact info was null or blank"); } + /** + * @return The request processor for NOTIFY requests which are validated and accepted to for this instance. The + * user may use this instance to add additional NOTIFY request processing strategies in order to provide extended + * capability of handling event packages in SIP. + */ + public RequestProcessor getNotifyProcessor() { + return notifyProcessor; + } } diff --git a/src/main/java/org/cafesip/sipunit/processing/RequestProcessingResult.java b/src/main/java/org/cafesip/sipunit/processing/RequestProcessingResult.java new file mode 100644 index 0000000000..f548a44164 --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/RequestProcessingResult.java @@ -0,0 +1,38 @@ +package org.cafesip.sipunit.processing; + +/** + * Contains provisional data on whether a request event was accepted by any {@link RequestProcessingStrategy} and if + * that processing step was successful. + *

+ * Created by TELES AG on 15/01/2018. + * + * @see RequestProcessor + */ +public class RequestProcessingResult { + + private final boolean isProcessed; + private final boolean isSuccessful; + + /** + * @param isProcessed If the input was accepted and processed + * @param isSuccessful If the processing result was successful + */ + public RequestProcessingResult(boolean isProcessed, boolean isSuccessful) { + this.isProcessed = isProcessed; + this.isSuccessful = isSuccessful; + } + + /** + * @return True if any strategy was able to accept the input of the request processor + */ + public boolean isProcessed() { + return isProcessed; + } + + /** + * @return True if the accepting strategy was able to successfully process the input of the request processor + */ + public boolean isSuccessful() { + return isSuccessful; + } +} diff --git a/src/main/java/org/cafesip/sipunit/processing/RequestProcessingStrategy.java b/src/main/java/org/cafesip/sipunit/processing/RequestProcessingStrategy.java new file mode 100644 index 0000000000..5654440e13 --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/RequestProcessingStrategy.java @@ -0,0 +1,62 @@ +package org.cafesip.sipunit.processing; + +import org.cafesip.sipunit.SipSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sip.RequestEvent; +import javax.sip.SipListener; + +/** + * The request matching strategy is used within every {@link SipListener} subclass which is bound to process an incoming + * request. + * In order to provide multiple ways of accepting a request from various sources, we introduce this type to provide an + * unified way to determine if a request fits a criterion for it to be accepted and processed within a session. + *

+ * Created by TELES AG on 12/01/2018. + * + * @see RequestProcessor + * @see SipSession#processRequest(RequestEvent) + */ +public abstract class RequestProcessingStrategy { + + protected static final Logger LOG = LoggerFactory.getLogger(RequestProcessingStrategy.class); + + private final boolean multipleInstanceAllowed; + + /** + * Initialize this strategy with multiple instances of this class allowed to be present in {@link RequestProcessor} + */ + public RequestProcessingStrategy() { + this(true); + } + + /** + * Initialize this strategy with option of multiple instances of this class to be present in {@link RequestProcessor} + * + * @param multipleInstanceAllowed If set to true, the processor will allow multiple instances of this class. Otherwise, + * any additional instances will not be added to the processor, and will be treated as + * a localized singleton. + */ + public RequestProcessingStrategy(boolean multipleInstanceAllowed) { + this.multipleInstanceAllowed = multipleInstanceAllowed; + } + + /** + * @return If true, the matcher will allow multiple instances of this class. Otherwise, any additional instances + * will not be added to the processor, and will be treated as a localized singleton. + */ + public final boolean multipleInstanceAllowed() { + return multipleInstanceAllowed; + } + + /** + * Determines if the inbound request is processed according to the criterion defined by this strategy. + * + * @param requestEvent The inbound request event + * @param receiver The governing receiver handler which received the request through its {@link org.cafesip.sipunit.SipStack} + * @return The result of the processing. A request may be accepted, but it may not be successful. If the request is accepted, + * no other strategies in the governing processor will execute. + */ + public abstract RequestProcessingResult processRequestEvent(final RequestEvent requestEvent, final ReceiverType receiver); +} diff --git a/src/main/java/org/cafesip/sipunit/processing/RequestProcessor.java b/src/main/java/org/cafesip/sipunit/processing/RequestProcessor.java new file mode 100644 index 0000000000..d819686df1 --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/RequestProcessor.java @@ -0,0 +1,216 @@ +package org.cafesip.sipunit.processing; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sip.RequestEvent; +import javax.sip.SipListener; +import javax.sip.message.Request; +import java.util.*; + +/** + * This class takes care that the inbound requests received by a governing {@link SipListener} subclass are tested + * against a configurable list of available strategies. The {@link RequestProcessingStrategy} instances which this class + * manages are in charge of testing an inbound SIP {@link Request} and providing feedback if the strategy was able to + * process the request successfully. The semantics of the (result of) processing is at the discretion of the implementing + * strategy. + *

+ * The request processor additionally manages that the request processing strategies do not produce side effects when being + * mutated, and provides concurrent access to the strategy handling mechanism. The default strategies which are provided by + * this class on instantiation are specified with the dedicated constructor. + *

+ * Created by TELES AG on 12/01/2018. + * + * @see RequestProcessingStrategy + */ +public class RequestProcessor { + + protected static final Logger LOG = LoggerFactory.getLogger(RequestProcessor.class); + + /** + * Request processing strategies process an incoming {@link Request} for the governing client after receiving + * the request through the stack. This class is initialized with a subset of initial strategies. + * The user of this library may add additional processing strategies in order to add additional processing options. + */ + private final List> availableStrategies = + Collections.synchronizedList(new ArrayList>()); + + /** + * Initialize this instance with the provided initial strategies + * + * @param initialStrategies Initial strategies which should be added to the instance + * @see RequestProcessor#add(RequestProcessingStrategy) + */ + public RequestProcessor(final RequestProcessingStrategy... initialStrategies) { + this(Arrays.asList(initialStrategies)); + } + + /** + * Initialize this instance with the provided initial strategies + * + * @param initialStrategies Initial strategies which should be added to the instance + * @see RequestProcessor#add(RequestProcessingStrategy) + */ + public RequestProcessor(final List> initialStrategies) { + synchronized (availableStrategies) { + for (RequestProcessingStrategy strategy : initialStrategies) { + add(strategy); + } + } + } + + /** + * Run all configured strategies in this matcher and determine if the request matches any configured criterion. The + * processor will execute every strategy in the available strategy list until the first strategy reports that the + * processing was successful. + * + * @param requestEvent The request being tested for a processing success with the available strategies + * @param receiver The governing object which received the request + * @return Result denoting if the request has been processed by any configured strategy, and if it was processed + * successfully + */ + public RequestProcessingResult processRequestEvent(final RequestEvent requestEvent, final ReceiverType receiver) { + RequestProcessingResult requestProcessingResult = new RequestProcessingResult(false, false); + + synchronized (availableStrategies) { + Iterator> iterator = availableStrategies.iterator(); + + // If we find a match, then the other strategies will not execute + while (!requestProcessingResult.isProcessed() && iterator.hasNext()) { + RequestProcessingStrategy strategy = iterator.next(); + requestProcessingResult = strategy.processRequestEvent(requestEvent, receiver); + + if(requestProcessingResult == null){ + LOG.warn("Request processing strategy " + strategy.getClass().getName() + " returned null"); + + requestProcessingResult = new RequestProcessingResult(false, false); + } + } + } + return requestProcessingResult; + } + + /** + * Add the strategy to be used in request processing in this processor. If the strategy is not permitted to add multiple + * instances and an existing instance of the same strategy class is present in the processor, it will not be added. + * Otherwise, the strategy will be added per the collection add behavior contract. + * + * @param requestProcessingStrategy The added request processing strategy + * @return If the request processing strategy was successfully added to the list of strategies used by this class + * @see List#add(Object) + */ + public boolean add(RequestProcessingStrategy requestProcessingStrategy) { + assertNotNull(requestProcessingStrategy); + + synchronized (availableStrategies) { + boolean permittedToAddAdditionalInstances = requestProcessingStrategy.multipleInstanceAllowed() || + !contains(requestProcessingStrategy.getClass()); + return permittedToAddAdditionalInstances && availableStrategies.add(requestProcessingStrategy); + } + } + + /** + * Removes the instance of the specified request processing strategy + * + * @param requestProcessingStrategy The strategy that needs to be removed by its reference in the request processor + * @return True if the specified instance of the searched strategy has been removed, false otherwise + * @see List#remove(Object) + */ + public boolean remove(RequestProcessingStrategy requestProcessingStrategy) { + assertNotNull(requestProcessingStrategy); + + synchronized (availableStrategies) { + if (availableStrategies.contains(requestProcessingStrategy) && availableStrategies.size() == 1) { + throw new IllegalArgumentException("Cannot remove only remaining strategy"); + } + + return availableStrategies.remove(requestProcessingStrategy); + } + } + + /** + * Removes any existing {@link RequestProcessingStrategy} defined by the searched class in the strategy list. + * If this is the only strategy in the strategy list before removal, the strategy list will be set to default, + * i.e. be reset with the default configured strategies. + * + * @param searchedClass The class that needs to be removed by its type in the request processor + * @return True if any instance of the searched strategy has been removed, false otherwise + */ + public boolean remove(Class> searchedClass) { + assertNotNull(searchedClass); + boolean isRemoved = false; + + synchronized (availableStrategies) { + Iterator> it = availableStrategies.iterator(); + + while (it.hasNext()) { + RequestProcessingStrategy current = it.next(); + + if (current.getClass().equals(searchedClass)) { + if (availableStrategies.size() == 1) { + throw new IllegalArgumentException("Cannot remove only remaining strategy"); + } + + isRemoved = true; + it.remove(); + } + } + } + + return isRemoved; + } + + /** + * Check if any existing {@link RequestProcessingStrategy} defined by the searched class is present in the configured + * strategy list. + * + * @param searchedClass The class whose direct instances will be searched for in the request class + * @return True if any instance of the searched strategy has been found, false otherwise + */ + public boolean contains(Class searchedClass) { + assertNotNull(searchedClass); + + synchronized (availableStrategies) { + for (RequestProcessingStrategy requestProcessingStrategy : availableStrategies) { + if (requestProcessingStrategy.getClass().equals(searchedClass)) { + return true; + } + } + } + + return false; + } + + /** + * Check if the {@link RequestProcessingStrategy} is added to this instance (by reference). + * + * @return True if the specified instance of the searched strategy has been found, false otherwise + * @see List#contains(Object) + */ + public boolean contains(RequestProcessingStrategy requestProcessingStrategy) { + assertNotNull(requestProcessingStrategy); + + return availableStrategies.contains(requestProcessingStrategy); + } + + /** + * @return A list of available strategies for incoming requests which this instance uses to process the inbound + * request. This list is unmodifiable and synchronized and will be updated with each change. + * @see Collections#synchronizedCollection(Collection) + */ + public List> getAvailableStrategies() { + return Collections.unmodifiableList(availableStrategies); + } + + private void assertNotNull(RequestProcessingStrategy requestProcessingStrategy) { + if(requestProcessingStrategy == null){ + throw new IllegalArgumentException("Request processing strategy may not be null"); + } + } + + private void assertNotNull(Class searchedClass) { + if(searchedClass == null){ + throw new IllegalArgumentException("Request processing strategy class may not be null"); + } + } +} diff --git a/src/main/java/org/cafesip/sipunit/processing/notify/ConferenceEventStrategy.java b/src/main/java/org/cafesip/sipunit/processing/notify/ConferenceEventStrategy.java new file mode 100644 index 0000000000..e51aae3b4c --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/notify/ConferenceEventStrategy.java @@ -0,0 +1,32 @@ +package org.cafesip.sipunit.processing.notify; + +import org.cafesip.sipunit.SipPhone; +import org.cafesip.sipunit.processing.RequestProcessingResult; +import org.cafesip.sipunit.processing.RequestProcessingStrategy; + +import javax.sip.RequestEvent; +import javax.sip.header.EventHeader; +import javax.sip.message.Request; + +/** + * Checks that the received event in {@link EventHeader} is a {@value ConferenceEventStrategy#EVENT_NAME} event. If that + * is the case, then the receiver object will continue the event handling (i.e. this handler does not do any other + * processing for this event). + *

+ * Created by TELES AG on 15/01/2018. + */ +public final class ConferenceEventStrategy extends RequestProcessingStrategy { + + private static final String EVENT_NAME = "conference"; + + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipPhone receiver) { + LOG.trace("Running " + this.getClass().getName()); + Request request = requestEvent.getRequest(); + EventHeader event = (EventHeader) request.getHeader(EventHeader.NAME); + + // Just return so that the test can use waitRequest() + boolean isValid = event != null && event.getEventType().equals(EVENT_NAME); + return new RequestProcessingResult(isValid, isValid); + } +} diff --git a/src/main/java/org/cafesip/sipunit/processing/notify/PresenceEventStrategy.java b/src/main/java/org/cafesip/sipunit/processing/notify/PresenceEventStrategy.java new file mode 100644 index 0000000000..9013b574e9 --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/notify/PresenceEventStrategy.java @@ -0,0 +1,44 @@ +package org.cafesip.sipunit.processing.notify; + +import org.cafesip.sipunit.PresenceSubscriber; +import org.cafesip.sipunit.SipPhone; +import org.cafesip.sipunit.processing.RequestProcessingResult; +import org.cafesip.sipunit.processing.RequestProcessingStrategy; + +import javax.sip.RequestEvent; +import javax.sip.header.EventHeader; +import javax.sip.header.FromHeader; +import javax.sip.message.Request; + +/** + * Checks that the received event in {@link EventHeader} is a {@value PresenceEventStrategy#EVENT_NAME} event. If it is, + * and the request targets one of the active {@link PresenceSubscriber} subscribers - then this strategy routes the + * event to the dedicated active subscriber object. + *

+ * Created by TELES AG on 15/01/2018. + */ +public final class PresenceEventStrategy extends RequestProcessingStrategy { + + private static final String EVENT_NAME = "presence"; + + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipPhone receiver) { + LOG.trace("Running " + this.getClass().getName()); + Request request = requestEvent.getRequest(); + + EventHeader event = (EventHeader) request.getHeader(EventHeader.NAME); + FromHeader from = (FromHeader) request.getHeader(FromHeader.NAME); + + if (event != null && event.getEventType().equals(EVENT_NAME)) { + PresenceSubscriber presenceSubscriber = receiver.getBuddyInfo(from.getAddress().getURI().toString()); + if (presenceSubscriber != null && presenceSubscriber.messageForMe(request)) { + presenceSubscriber.processEvent(requestEvent); + return new RequestProcessingResult(true, true); + } + + return new RequestProcessingResult(true, false); + } + + return new RequestProcessingResult(false, true); + } +} diff --git a/src/main/java/org/cafesip/sipunit/processing/notify/ReferEventStrategy.java b/src/main/java/org/cafesip/sipunit/processing/notify/ReferEventStrategy.java new file mode 100644 index 0000000000..3b4f68dbe4 --- /dev/null +++ b/src/main/java/org/cafesip/sipunit/processing/notify/ReferEventStrategy.java @@ -0,0 +1,47 @@ +package org.cafesip.sipunit.processing.notify; + +import org.cafesip.sipunit.ReferSubscriber; +import org.cafesip.sipunit.SipPhone; +import org.cafesip.sipunit.processing.RequestProcessingResult; +import org.cafesip.sipunit.processing.RequestProcessingStrategy; + +import javax.sip.Dialog; +import javax.sip.RequestEvent; +import javax.sip.header.EventHeader; +import javax.sip.message.Request; +import java.util.List; + +/** + * Checks that the received event in {@link EventHeader} is a {@value ReferEventStrategy#EVENT_NAME} event. If it is, + * and the request targets one of the active {@link ReferSubscriber} subscribers - then this strategy routes the + * event to the dedicated active subscriber object. + *

+ * Created by TELES AG on 15/01/2018. + */ +public final class ReferEventStrategy extends RequestProcessingStrategy { + + private static final String EVENT_NAME = "refer"; + + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipPhone receiver) { + LOG.trace("Running " + this.getClass().getName()); + Request request = requestEvent.getRequest(); + Dialog dialog = requestEvent.getDialog(); + + EventHeader event = (EventHeader) request.getHeader(EventHeader.NAME); + + if (event != null && dialog != null && event.getEventType().equals(EVENT_NAME)) { + List refers = receiver.getRefererInfoByDialog(dialog.getDialogId()); + for (ReferSubscriber referSubscriber : refers) { + if (referSubscriber.messageForMe(request)) { + referSubscriber.processEvent(requestEvent); + return new RequestProcessingResult(true, true); + } + } + + return new RequestProcessingResult(true, false); + } + + return new RequestProcessingResult(false, false); + } +} \ No newline at end of file diff --git a/src/test/java/org/cafesip/sipunit/test/misc/TestRequestProcessing.java b/src/test/java/org/cafesip/sipunit/test/misc/TestRequestProcessing.java new file mode 100644 index 0000000000..8d87adfd2c --- /dev/null +++ b/src/test/java/org/cafesip/sipunit/test/misc/TestRequestProcessing.java @@ -0,0 +1,171 @@ +package org.cafesip.sipunit.test.misc; + +import org.cafesip.sipunit.SipSession; +import org.cafesip.sipunit.processing.RequestProcessingResult; +import org.cafesip.sipunit.processing.RequestProcessingStrategy; +import org.cafesip.sipunit.processing.RequestProcessor; +import org.junit.Before; +import org.junit.Test; + +import javax.sip.RequestEvent; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Test behavior of processing processing on the configuration of {@link org.cafesip.sipunit.processing.RequestProcessingStrategy}. + *

+ * Created by TELES AG on 12/01/2018. + */ +public class TestRequestProcessing { + + private RequestProcessingStrategy defaultRequestProcessingStrategy = new RequestProcessingStrategy(false) { + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipSession receiver) { + return new RequestProcessingResult(false, false); + } + }; + + private RequestProcessor requestProcessor; + + @Before + public void setUp() { + requestProcessor = new RequestProcessor(defaultRequestProcessingStrategy); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotInitializeWithNull() { + new RequestProcessor<>((RequestProcessingStrategy) null); + } + + /** + * The request processor should not allow any mutation because of the immutable list + */ + @Test(expected = UnsupportedOperationException.class) + public void testGetStrategiesMutation() { + requestProcessor.getAvailableStrategies().clear(); + } + + /** + * If the strategy list has only the default strategy and this strategy is attempted to be removed, the processor + * should throw an exception to prevent side effects + */ + @Test(expected = IllegalArgumentException.class) + public void testRemoveLastStrategyClass() { + requestProcessor.remove(defaultRequestProcessingStrategy.getClass()); + } + + /** + * If the strategy list has only the default strategy and this strategy is attempted to be removed, the processor + * should throw an exception to prevent side effects + */ + @Test(expected = IllegalArgumentException.class) + public void testRemoveLastStrategyInstance() { + requestProcessor.remove(defaultRequestProcessingStrategy); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotAddNull() { + requestProcessor.add(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotRemoveNullInstance() { + requestProcessor.remove((RequestProcessingStrategy) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotRemoveNullClass() { + requestProcessor.remove((Class) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotSearchForNullInstance() { + requestProcessor.contains((RequestProcessingStrategy) null); + } + + @Test(expected = IllegalArgumentException.class) + public void testShouldNotSearchForNullClass() { + requestProcessor.contains((Class) null); + } + + @Test + public void testStrategiesMutation() { + // Attempt mutating the list obtained through the getter + // Should have 1 default processing strategies + List requestProcessingStrategies = requestProcessor.getAvailableStrategies(); + assertEquals(1, requestProcessingStrategies.size()); + + // Create a dummy strategy + RequestProcessingStrategy newStrategy = createMockStrategy(); + + // Mutate strategies through the accessor + requestProcessor.add(newStrategy); + // Returned list should be updated + assertEquals(2, requestProcessingStrategies.size()); + + List newMatchingStrategies = requestProcessor.getAvailableStrategies(); + assertEquals(2, newMatchingStrategies.size()); + assertEquals(newStrategy, newMatchingStrategies.get(1)); + + assertTrue(requestProcessor.contains(newStrategy)); + assertTrue(requestProcessor.contains(newStrategy.getClass())); + + // Default strategy + assertTrue(requestProcessor.contains(defaultRequestProcessingStrategy.getClass())); + } + + @Test + public void testMultipleInstancesAllowed() { + RequestProcessingStrategy multipleInstancesStrategy = createMockStrategy(); + + assertFalse(defaultRequestProcessingStrategy.multipleInstanceAllowed()); + assertTrue(multipleInstancesStrategy.multipleInstanceAllowed()); + + assertEquals(1, requestProcessor.getAvailableStrategies().size()); + assertTrue(requestProcessor.contains(defaultRequestProcessingStrategy.getClass())); + + assertTrue(requestProcessor.add(multipleInstancesStrategy)); + assertEquals(2, requestProcessor.getAvailableStrategies().size()); + + // Already has this strategy by now + assertFalse(requestProcessor.add(defaultRequestProcessingStrategy)); + assertEquals(2, requestProcessor.getAvailableStrategies().size()); + + // Add a multiple instance strategy + assertTrue(requestProcessor.add(multipleInstancesStrategy)); + assertEquals(3, requestProcessor.getAvailableStrategies().size()); + + assertTrue(requestProcessor.add(multipleInstancesStrategy)); + assertEquals(4, requestProcessor.getAvailableStrategies().size()); + } + + @Test + public void testShouldConvertNullResultToFailed() { + RequestProcessingStrategy nullStrategy = new RequestProcessingStrategy() { + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipSession receiver) { + return null; + } + }; + requestProcessor.add(nullStrategy); + + RequestProcessingResult result = requestProcessor.processRequestEvent(new RequestEvent(this, null, null, null), null); + assertNotNull(result); + assertFalse(result.isProcessed()); + assertFalse(result.isSuccessful()); + } + + private RequestProcessingStrategy createMockStrategy() { + return createMockStrategy(true); + } + + private RequestProcessingStrategy createMockStrategy(boolean multipleInstancesAllowed) { + return new RequestProcessingStrategy(multipleInstancesAllowed) { + @Override + public RequestProcessingResult processRequestEvent(RequestEvent requestEvent, SipSession receiver) { + return new RequestProcessingResult(false, false); + } + }; + } +} diff --git a/src/test/java/org/cafesip/sipunit/test/noproxy/TestReferNoProxy.java b/src/test/java/org/cafesip/sipunit/test/noproxy/TestReferNoProxy.java index e57cc9e75d..2942c8988e 100644 --- a/src/test/java/org/cafesip/sipunit/test/noproxy/TestReferNoProxy.java +++ b/src/test/java/org/cafesip/sipunit/test/noproxy/TestReferNoProxy.java @@ -1701,7 +1701,7 @@ public void testNotifyTimeouts() throws Exception { // prepare the far end to respond to unSUBSCRIBE ub.processSubscribe(5000, SipResponse.OK, "OK Done"); // send the un-SUBSCRIBE - assertTrue(subscription.unsubscribe(100)); + assertTrue(subscription.unsubscribe(1000)); assertFalse(subscription.isRemovalComplete()); assertEquals(SipResponse.OK, subscription.getReturnCode()); assertTrue(subscription.isSubscriptionTerminated());