View Javadoc

1   /*
2      Licensed under the Apache License, Version 2.0 (the "License");
3      you may not use this file except in compliance with the License.
4      You may obtain a copy of the License at
5   
6        http://www.apache.org/licenses/LICENSE-2.0
7   
8      Unless required by applicable law or agreed to in writing, software
9      distributed under the License is distributed on an "AS IS" BASIS,
10     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11     See the License for the specific language governing permissions and
12     limitations under the License.
13  */
14  package uk.nhs.interoperability.transport.WS;
15  
16  import java.io.BufferedInputStream;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.PrintWriter;
20  import java.net.HttpURLConnection;
21  import java.net.MalformedURLException;
22  import java.net.SocketTimeoutException;
23  import java.net.URL;
24  import java.net.URLConnection;
25  
26  import javax.servlet.http.HttpServletResponse;
27  import javax.xml.parsers.ParserConfigurationException;
28  import javax.xml.xpath.XPathConstants;
29  import javax.xml.xpath.XPathExpressionException;
30  
31  import org.w3c.dom.Document;
32  import org.w3c.dom.Node;
33  import org.xml.sax.SAXException;
34  
35  import uk.nhs.interoperability.infrastructure.ITKCommsException;
36  import uk.nhs.interoperability.infrastructure.ITKMessagingException;
37  import uk.nhs.interoperability.infrastructure.ITKTransportTimeoutException;
38  import uk.nhs.interoperability.payload.ITKMessage;
39  import uk.nhs.interoperability.payload.SimpleMessage;
40  import uk.nhs.interoperability.transport.ITKSender;
41  import uk.nhs.interoperability.transport.ITKTransportRoute;
42  import uk.nhs.interoperability.util.ITKApplicationProperties;
43  import uk.nhs.interoperability.util.Logger;
44  import uk.nhs.interoperability.util.xml.DomUtils;
45  import uk.nhs.interoperability.util.xml.XPaths;
46  
47  import com.xmlsolutions.annotation.Requirement;
48  
49  /**
50   * The Class ITKSenderWSImpl.
51   *
52   * @author Michael Odling-Smee
53   * @author Nicholas Jones
54   * @since 0.1
55   */
56  public class ITKSenderWSImpl implements ITKSender {
57  
58  	/** The Constant WSSOAP_FROM. */
59  	private static final String WSSOAP_FROM = "wssoap.from";
60  	
61  	/** The Constant WSSECURITY_USERNAME. */
62  	private static final String WSSECURITY_USERNAME = "wssecurity.username";
63  
64  	/* (non-Javadoc)
65  	 * @see uk.nhs.interoperability.transport.ITKSender#send(uk.nhs.interoperability.transport.ITKTransportRoute, uk.nhs.interoperability.payload.ITKMessage)
66  	 */
67  	@Override
68  	public void send(ITKTransportRoute destination, ITKMessage request)
69  			throws ITKMessagingException {
70  
71  		// Add the soap wrappers
72  		WSSOAPMessageImpl message = null;
73  		// TODO: Review this decision. The destination is presumed fully formed by this point (even if from pre-determined route)
74  		//       - therefore the only difference in the SOAP message construction is in the message type.
75  		//       This means that the constructor without the destination is not required, simplifying the WSSOAPMessageImpl
76  		if (request.isResponse()) {
77  			message = new WSSOAPMessageImpl(destination, request, WSSOAPMessageImpl.ASYNCRESP);
78  		} else {
79  			message = new WSSOAPMessageImpl(destination, request, WSSOAPMessageImpl.SYNCREQ);
80  		}
81  		//WSSOAPMessageImpl message = new WSSOAPMessageImpl(destination, request, WSSOAPMessageImpl.SYNCREQ);
82  		message.setFrom(ITKApplicationProperties.getProperty(WSSOAP_FROM));
83  		message.setTo(destination.getPhysicalAddress());
84  		message.setUsername(ITKApplicationProperties.getProperty(WSSECURITY_USERNAME));
85  		message.setTimeToLive(destination.getTimeToLive());
86  		
87  		String SOAPPayload = message.getFullPayload();
88  			
89  		Document responseDoc = transportSend(destination, SOAPPayload, message.getAction());
90  		
91  		if (responseDoc==null){
92  		
93  			// No responseDoc means the call received a 202. Just pass back to the application.
94  			
95  		} else {
96  		
97  			// If a responseDoc is received, this is an error
98  			// TODO: Should SMR be considered OK in this scenario??
99  
100 			ITKMessagingException unknownResponseException = new ITKMessagingException(
101 						ITKMessagingException.PROCESSING_ERROR_NOT_RETRYABLE_CODE, "Unexpected Response");
102 			Logger.error("Unexpected Response",unknownResponseException);
103 			Logger.trace(DomUtils.serialiseToXML(responseDoc, DomUtils.PRETTY_PRINT));
104 			throw unknownResponseException;
105 		
106 		}
107 
108 	
109 	}
110 
111 	/* (non-Javadoc)
112 	 * @see uk.nhs.interoperability.transport.ITKSender#sendSync(uk.nhs.interoperability.transport.ITKTransportRoute, uk.nhs.interoperability.payload.ITKMessage)
113 	 */
114 	@Override
115 	@Requirement(traceTo={"WS-PAT-01"})
116 	public ITKMessage sendSync(ITKTransportRoute destination, ITKMessage request)
117 			throws ITKMessagingException {
118 
119 		// Add the soap wrappers
120 		WSSOAPMessageImpl message = new WSSOAPMessageImpl(destination, request, WSSOAPMessageImpl.SYNCREQ);
121 		message.setFrom(ITKApplicationProperties.getProperty(WSSOAP_FROM));
122 		message.setTo(destination.getPhysicalAddress());
123 		message.setUsername(ITKApplicationProperties.getProperty(WSSECURITY_USERNAME));
124 		message.setTimeToLive(destination.getTimeToLive());
125 		//Obtain the on-the-wire message
126 		String SOAPPayload = message.getFullPayload();
127 		
128 		try {
129 			
130 			Document responseDoc = transportSend(destination, SOAPPayload, message.getAction());
131 			
132 			if (responseDoc == null) {
133 				// No responseDoc means the call received a 202 - this is an exception for Synchronous.
134 				ITKMessagingException unknownResponseException = new ITKMessagingException(ITKMessagingException.PROCESSING_ERROR_NOT_RETRYABLE_CODE, "HTTP Acknowledgement only received (202).");
135 				Logger.error("Unknown response type",unknownResponseException);
136 				throw unknownResponseException;
137 				
138 			} else {
139 				
140 				// Pretty print the response message
141 				Logger.trace(DomUtils.serialiseToXML(responseDoc, DomUtils.PRETTY_PRINT));
142 
143 				// Extract the SOAP Body Content
144 				Document businessPayloadDocument = DomUtils.createDocumentFromNode((Node)XPaths.SOAP_BODY_CONTENT_XPATH.evaluate(responseDoc, XPathConstants.NODE));
145 
146 				if (businessPayloadDocument == null){
147 					// exception
148 					ITKMessagingException unknownResponseException = new ITKMessagingException(
149 							ITKMessagingException.PROCESSING_ERROR_NOT_RETRYABLE_CODE, "No payload in SOAP Body");
150 					Logger.error("Unknown response type",unknownResponseException);
151 					throw unknownResponseException;
152 				}
153 				
154 				String responsePayloadString = DomUtils.serialiseToXML(businessPayloadDocument);
155 				
156 				//Construct the appropriate object for handing over to the application
157 				return new SimpleMessage(responsePayloadString);
158 
159 			}
160 		} catch (ParserConfigurationException pce) {
161 			Logger.error("ParseConfigurationException on WS-CALL", pce);
162 			throw new ITKCommsException("XML Configuration Error Processing ITK Response");
163 		} catch (XPathExpressionException xpe) {
164 			Logger.error("XPathExpressionException reading payload on WS Response", xpe);
165 			throw new ITKCommsException("No Payload found in ITK Response");
166 		}
167 	
168 	}
169 
170 	
171 	/* (non-Javadoc)
172 	 * @see uk.nhs.interoperability.transport.ITKSender#sendAysnc(uk.nhs.interoperability.transport.ITKTransportRoute, uk.nhs.interoperability.payload.ITKMessage)
173 	 */
174 	@Override
175 	@Requirement(traceTo={"WS-PAT-02"})
176 	public void sendAysnc(ITKTransportRoute destination, ITKMessage request)
177 			throws ITKMessagingException {
178 		
179 		// Add the soap wrappers
180 		WSSOAPMessageImpl message = null;
181 		
182 		// Message type is always ASYNCREQ here because
183 		// 1) True ASYNC responses (i.e. ADT Query Resp) should be sent using a response message on the Send API
184 		//    (response message is flagged by creating using inbound message properties)
185 		// 2) INF and BUS ACKS are actually new ASYNCREQ messages. Although they may be flagged as a response message
186 		//    (as they are created using inbound message properties) this is only for the purposes of routing.
187 		message = new WSSOAPMessageImpl(destination, request, WSSOAPMessageImpl.ASYNCREQ);
188 
189 		message.setFrom(ITKApplicationProperties.getProperty(WSSOAP_FROM));
190 		message.setTo(destination.getPhysicalAddress());
191 		message.setUsername(ITKApplicationProperties.getProperty(WSSECURITY_USERNAME));
192 		message.setTimeToLive(destination.getTimeToLive());
193 		//Obtain the on-the-wire message
194 		String SOAPPayload = message.getFullPayload();
195 		
196 		try {
197 			
198 			Document responseDoc = transportSend(destination, SOAPPayload, message.getAction());
199 			
200 			if (responseDoc == null) {
201 				// No responseDoc means the call received a HTTP 202. OK for ASYNC				
202 			} else {
203 				
204 				//Obtain the actual business payload - should be SMR or else this is an error.
205 				Document businessPayload = DomUtils.createDocumentFromNode((Node)XPaths.SOAP_WRAPPED_ITK_SIMPLE_MESSAGE_RESPONSE_XPATH.evaluate(responseDoc, XPathConstants.NODE));
206 				
207 				// If no known payload then throw an exception
208 				if (businessPayload==null){
209 					ITKMessagingException unknownResponseException = new ITKMessagingException("Unknown Response Type");
210 					Logger.error("Unknown response type",unknownResponseException);
211 					Logger.trace(DomUtils.serialiseToXML(businessPayload, DomUtils.PRETTY_PRINT));
212 					throw unknownResponseException;
213 				}
214 				
215 			}
216 			
217 		} catch (ParserConfigurationException pce) {
218 			Logger.error("ParseConfigurationException on WS-CALL", pce);
219 			throw new ITKCommsException("XML Configuration Error Processing ITK Response");
220 		} catch (XPathExpressionException xpe) {
221 			Logger.error("XPathExpressionException reading payload on WS Response", xpe);
222 			throw new ITKCommsException("No Payload found in ITK Response");
223 		}
224 	
225 	}
226 	
227 	/**
228 	 * Transport send.
229 	 *
230 	 * @param destination the destination
231 	 * @param SOAPPayload the sOAP payload
232 	 * @return the document
233 	 * @throws ITKMessagingException the iTK messaging exception
234 	 */
235 	@Requirement(traceTo={"WS-PAT-01","WS-PAT-02"})
236 	private Document transportSend(ITKTransportRoute destination, String SOAPPayload, String SOAPAction)
237 			throws ITKMessagingException {
238 		
239 		Document responseDoc = null;
240 		// Post the msg
241 		String serviceEndpoint = destination.getPhysicalAddress();
242 		try {
243 			URLConnection urlConnection = 
244 					new URL(serviceEndpoint).openConnection();
245 			HttpURLConnection conn = (HttpURLConnection) urlConnection;
246 			conn.setUseCaches(false);
247 			conn.setDoOutput(true);
248 			conn.setDoInput(true);
249 			conn.setRequestMethod("POST");
250 			conn.setRequestProperty("Content-type","text/xml");
251 			conn.setRequestProperty("accept-charset","UTF-8");
252 			conn.setRequestProperty("SOAPAction",SOAPAction);
253 			// TODO: Configure this. By instance/service?
254 			conn.setConnectTimeout(30000); 
255 			PrintWriter pw = new PrintWriter(conn.getOutputStream());
256 			pw.write(SOAPPayload);
257 			pw.close();
258 			int responseCode = conn.getResponseCode();
259 			Logger.trace("HTTP Response Code:"+responseCode);
260 			if (responseCode == HttpServletResponse.SC_ACCEPTED){
261 				
262 				Logger.trace("SIMPLE HTTP ACCEPT (202)");
263 				
264 			} else if (responseCode == HttpServletResponse.SC_OK) {
265 				Logger.trace("HTTP 200");
266 				String responseString = readInput(conn.getInputStream());
267 				responseDoc = DomUtils.parse(responseString);
268 				
269 			} else if (responseCode == HttpServletResponse.SC_INTERNAL_SERVER_ERROR) {
270 				Logger.trace("HTTP 500");
271 				String responseString = readInput(conn.getErrorStream());
272 				//Understand why an error has occurred - do we have a SOAP fault?				
273 				if (responseString != null && responseString.contains("http://www.w3.org/2005/08/addressing/fault")) {
274 					//SOAP fault
275 					throw ITKSOAPException.parseSOAPFault(responseString);
276 				} else {
277 					throw new ITKCommsException("HTTP Internal server error");
278 				}
279 			} else {
280 				
281 				Logger.trace("Unrecognized HTTP response code:"+responseCode);
282 				throw new ITKCommsException("Unrecognized HTTP response code:"+responseCode);
283 
284 			}
285 			
286 		} catch (MalformedURLException mue) {
287 			Logger.error("MalformedURLException on WS-CALL", mue);
288 			throw new ITKCommsException("Configuration error sending ITK Message");
289 		} catch (SocketTimeoutException ste) {
290 			Logger.error("Timeout on WS-CALL", ste);
291 			throw new ITKTransportTimeoutException("Transport timeout sending ITK Message");
292 		} catch (IOException ioe) {
293 			Logger.error("IOException on WS-CALL", ioe);
294 			throw new ITKCommsException("Transport error sending ITK Message");
295 		} catch (SAXException se) {
296 			Logger.error("SAXException processing response from WS-CALL", se);
297 			throw new ITKCommsException("XML Error Processing ITK Response");
298 		} catch (ParserConfigurationException pce) {
299 			Logger.error("ParseConfigurationException on WS-CALL", pce);
300 			throw new ITKCommsException("XML Configuration Error Processing ITK Response");
301 		}
302 		
303 		return responseDoc;
304 		
305 	}
306 	
307 	/**
308 	 * Read input.
309 	 *
310 	 * @param is the is
311 	 * @return the string
312 	 * @throws IOException Signals that an I/O exception has occurred.
313 	 */
314 	private String readInput(InputStream is) throws IOException {
315 		BufferedInputStream bis = new BufferedInputStream(is);
316 		byte[] contents = new byte[1024];
317 		int bytesRead = 0;
318 		String responseString = "";
319 		while ((bytesRead = bis.read(contents)) != -1) {
320 			responseString = responseString + new String(contents, 0, bytesRead);
321 		}
322 		Logger.trace("Response was:"+responseString);
323 		bis.close();
324 		
325 		return responseString ;
326 	}
327 }