001package ca.uhn.fhir.jpa.subscription.match.deliver.websocket; 002 003/* 004 * #%L 005 * HAPI FHIR Subscription Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry; 026import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelWithHandlers; 027import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; 028import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage; 029import org.hl7.fhir.instance.model.api.IIdType; 030import org.hl7.fhir.r4.model.IdType; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033import org.springframework.beans.factory.annotation.Autowired; 034import org.springframework.messaging.Message; 035import org.springframework.messaging.MessageHandler; 036import org.springframework.messaging.MessagingException; 037import org.springframework.web.socket.CloseStatus; 038import org.springframework.web.socket.TextMessage; 039import org.springframework.web.socket.WebSocketHandler; 040import org.springframework.web.socket.WebSocketSession; 041import org.springframework.web.socket.handler.TextWebSocketHandler; 042 043import javax.annotation.PostConstruct; 044import javax.annotation.PreDestroy; 045import java.io.IOException; 046 047public class SubscriptionWebsocketHandler extends TextWebSocketHandler implements WebSocketHandler { 048 private static Logger ourLog = LoggerFactory.getLogger(SubscriptionWebsocketHandler.class); 049 @Autowired 050 protected WebsocketConnectionValidator myWebsocketConnectionValidator; 051 @Autowired 052 SubscriptionChannelRegistry mySubscriptionChannelRegistry; 053 054 /** 055 * Constructor 056 */ 057 public SubscriptionWebsocketHandler() { 058 super(); 059 } 060 061 private IState myState = new InitialState(); 062 063 @Override 064 public void afterConnectionClosed(WebSocketSession theSession, CloseStatus theStatus) throws Exception { 065 super.afterConnectionClosed(theSession, theStatus); 066 ourLog.info("Closing WebSocket connection from {}", theSession.getRemoteAddress()); 067 } 068 069 @Override 070 public void afterConnectionEstablished(WebSocketSession theSession) throws Exception { 071 super.afterConnectionEstablished(theSession); 072 ourLog.info("Incoming WebSocket connection from {}", theSession.getRemoteAddress()); 073 } 074 075 protected void handleFailure(Exception theE) { 076 ourLog.error("Failure during communication", theE); 077 } 078 079 @Override 080 protected void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) throws Exception { 081 ourLog.info("Textmessage: " + theMessage.getPayload()); 082 myState.handleTextMessage(theSession, theMessage); 083 } 084 085 @Override 086 public void handleTransportError(WebSocketSession theSession, Throwable theException) throws Exception { 087 super.handleTransportError(theSession, theException); 088 ourLog.error("Transport error", theException); 089 } 090 091 @PostConstruct 092 public synchronized void postConstruct() { 093 ourLog.info("Websocket connection has been created"); 094 } 095 096 @PreDestroy 097 public synchronized void preDescroy() { 098 ourLog.info("Websocket connection is closing"); 099 IState state = myState; 100 if (state != null) { 101 state.closing(); 102 } 103 } 104 105 106 private interface IState { 107 108 void closing(); 109 110 void handleTextMessage(WebSocketSession theSession, TextMessage theMessage); 111 112 } 113 114 private class BoundStaticSubscriptionState implements IState, MessageHandler { 115 116 private final WebSocketSession mySession; 117 private final ActiveSubscription myActiveSubscription; 118 119 public BoundStaticSubscriptionState(WebSocketSession theSession, ActiveSubscription theActiveSubscription) { 120 mySession = theSession; 121 myActiveSubscription = theActiveSubscription; 122 123 SubscriptionChannelWithHandlers subscriptionChannelWithHandlers = mySubscriptionChannelRegistry.getDeliveryReceiverChannel(theActiveSubscription.getChannelName()); 124 subscriptionChannelWithHandlers.addHandler(this); 125 } 126 127 @Override 128 public void closing() { 129 SubscriptionChannelWithHandlers subscriptionChannelWithHandlers = mySubscriptionChannelRegistry.getDeliveryReceiverChannel(myActiveSubscription.getChannelName()); 130 subscriptionChannelWithHandlers.removeHandler(this); 131 } 132 133 private void deliver() { 134 try { 135 String payload = "ping " + myActiveSubscription.getId(); 136 ourLog.info("Sending WebSocket message: {}", payload); 137 mySession.sendMessage(new TextMessage(payload)); 138 } catch (IOException e) { 139 handleFailure(e); 140 } 141 } 142 143 @Override 144 public void handleMessage(Message<?> theMessage) { 145 if (!(theMessage.getPayload() instanceof ResourceDeliveryMessage)) { 146 return; 147 } 148 try { 149 ResourceDeliveryMessage msg = (ResourceDeliveryMessage) theMessage.getPayload(); 150 if (myActiveSubscription.getSubscription().equals(msg.getSubscription())) { 151 deliver(); 152 } 153 } catch (Exception e) { 154 ourLog.error("Failure handling subscription payload", e); 155 throw new MessagingException(theMessage, Msg.code(6) + "Failure handling subscription payload", e); 156 } 157 } 158 159 @Override 160 public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { 161 try { 162 theSession.sendMessage(new TextMessage("Unexpected client message: " + theMessage.getPayload())); 163 } catch (IOException e) { 164 handleFailure(e); 165 } 166 } 167 } 168 169 private class InitialState implements IState { 170 171 private IIdType bindSimple(WebSocketSession theSession, String theBindString) { 172 IdType id = new IdType(theBindString); 173 174 WebsocketValidationResponse response = myWebsocketConnectionValidator.validate(id); 175 if (!response.isValid()) { 176 try { 177 ourLog.warn(response.getMessage()); 178 theSession.close(new CloseStatus(CloseStatus.PROTOCOL_ERROR.getCode(), response.getMessage())); 179 } catch (IOException e) { 180 handleFailure(e); 181 } 182 return null; 183 } 184 185 myState = new BoundStaticSubscriptionState(theSession, response.getActiveSubscription()); 186 187 return id; 188 } 189 190 @Override 191 public void closing() { 192 // nothing 193 } 194 195 @Override 196 public void handleTextMessage(WebSocketSession theSession, TextMessage theMessage) { 197 String message = theMessage.getPayload(); 198 if (message.startsWith("bind ")) { 199 String remaining = message.substring("bind ".length()); 200 201 IIdType subscriptionId; 202 subscriptionId = bindSimple(theSession, remaining); 203 if (subscriptionId == null) { 204 return; 205 } 206 207 try { 208 theSession.sendMessage(new TextMessage("bound " + subscriptionId.getIdPart())); 209 } catch (IOException e) { 210 handleFailure(e); 211 } 212 213 } 214 } 215 216 } 217 218}