diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java index 5d1ee2a..f592fd1 100644 --- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java +++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/core/SmesMessageSpecification.java @@ -24,7 +24,7 @@ import static org.springframework.integration.smpp.core.SmppConstants.*; */ public class SmesMessageSpecification { - private Log log = LogFactory.getLog(getClass()); + private static Log log = LogFactory.getLog(SmesMessageSpecification.class); private TimeFormatter timeFormatter = new AbsoluteTimeFormatter(); private int maxLengthSmsMessages = 140; @@ -108,7 +108,7 @@ public class SmesMessageSpecification { * @return a {@link SmesMessageSpecification} */ public static SmesMessageSpecification fromMessage(ClientSession smppSession, Message msg) { - System.out.println("Message: "+msg); + if (log.isDebugEnabled()) log.debug("Message: "+msg); String srcAddy = valueIfHeaderExists(SRC_ADDR, msg); String dstAddy = valueIfHeaderExists(DST_ADDR, msg); String smsTxt = valueIfHeaderExists(SMS_MSG, msg); @@ -203,8 +203,8 @@ public class SmesMessageSpecification { /** * tries to safely extract the ESMClass - * @param im - * @return + * @param im message + * @return esm class */ static private ESMClass esmClassFromHeader( Message im){ String h = ESM_CLASS ; diff --git a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/inbound/SmppInboundGateway.java b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/inbound/SmppInboundGateway.java index 7e991c5..4904851 100644 --- a/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/inbound/SmppInboundGateway.java +++ b/spring-integration-smpp/src/main/java/org/springframework/integration/smpp/inbound/SmppInboundGateway.java @@ -78,8 +78,7 @@ public class SmppInboundGateway extends MessagingGatewaySupport { Message response = sendAndReceiveMessage(msg); logger.debug("received a reply message; will handle as in outbound adapter"); - // todo copy all the code from the outbound adapter related to defaults - /// todo also make sure that we simply flip the inbound to outbound + /// todo figure out relationship between inbound-gw and replyChannel applyDefaults(msg, response, SmesMessageSpecification.fromMessage(smppSession, response)).send(); logger.debug("the reply SMS message has been sent."); } @@ -88,8 +87,8 @@ public class SmppInboundGateway extends MessagingGatewaySupport { /** * among other things this method simply 'flips' the src/dst * - * @param request req - * @param response res + * @param request req + * @param response res * @param smesMessageSpecification spec * @return same spec reflecting new switches */ @@ -104,7 +103,7 @@ public class SmppInboundGateway extends MessagingGatewaySupport { if (request.getHeaders().containsKey(SmppConstants.DEST_ADDRESS)) { from = (String) request.getHeaders().get(SmppConstants.DEST_ADDRESS); if (StringUtils.hasText(from)) - smesMessageSpecification.setSourceAddressIfRequired(from); + smesMessageSpecification.setSourceAddress(from); } if (defaultSourceAddressTypeOfNumber != null) smesMessageSpecification.setSourceAddressTypeOfNumberIfRequired(this.defaultSourceAddressTypeOfNumber); diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java new file mode 100644 index 0000000..4d6ff19 --- /dev/null +++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/MockSmppServer.java @@ -0,0 +1,342 @@ +package org.springframework.integration.smpp; + +import org.jsmpp.PDUStringException; +import org.jsmpp.SMPPConstant; +import org.jsmpp.bean.*; +import org.jsmpp.extra.ProcessRequestException; +import org.jsmpp.extra.SessionState; +import org.jsmpp.session.*; +import org.jsmpp.util.DeliveryReceiptState; +import org.jsmpp.util.MessageIDGenerator; +import org.jsmpp.util.MessageId; +import org.jsmpp.util.RandomMessageIDGenerator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; + +/** + * This is mock SMPP server connection which copied from jsmpp-examples (git clone https://github.com/otnateos/jsmpp.git) + * with additional functionality: + * + * @author Johanes Soetanto + * @since 2.2 + */ +public class MockSmppServer extends ServerResponseDeliveryAdapter implements Runnable, ServerMessageReceiverListener { + private static final Logger logger = LoggerFactory.getLogger(MockSmppServer.class); + private String systemId; + private String password; + private int port; + private Map connectionSessionMap = new HashMap(); + + private final ExecutorService execService = Executors.newFixedThreadPool(5); + private final ExecutorService execServiceDelReceipt = Executors.newFixedThreadPool(100); + private final MessageIDGenerator messageIDGenerator = new RandomMessageIDGenerator(); + + public MockSmppServer(int port, String systemId, String password) throws IOException { + this.systemId = systemId; + this.password = password; + this.port = port; + } + + public void run() { + try { + SMPPServerSessionListener sessionListener = new SMPPServerSessionListener(port); + logger.info("Listening on port {}", port); + while (true) { + SMPPServerSession serverSession = sessionListener.accept(); + logger.info("Accepting connection for session {}", serverSession.getSessionId()); + serverSession.setMessageReceiverListener(this); + serverSession.setResponseDeliveryListener(this); + execService.execute(new WaitBindTask(serverSession, systemId, password, connectionSessionMap)); + } + } catch (IOException e) { + logger.error("IO error occurred", e); + } + } + + public QuerySmResult onAcceptQuerySm(QuerySm querySm, + SMPPServerSession source) throws ProcessRequestException { + logger.info("Accepting query sm, but not implemented"); + return null; + } + + public MessageId onAcceptSubmitSm(SubmitSm submitSm, + SMPPServerSession source) throws ProcessRequestException { + MessageId messageId = messageIDGenerator.newMessageId(); + logger.debug("Receiving submit_sm '{}', and will return message id {}", + new String(submitSm.getShortMessage()), messageId); + if (SMSCDeliveryReceipt.SUCCESS.containedIn(submitSm.getRegisteredDelivery()) + || SMSCDeliveryReceipt.SUCCESS_FAILURE.containedIn(submitSm.getRegisteredDelivery())) { + execServiceDelReceipt.execute(new DeliveryReceiptTask(source, submitSm, messageId)); + + } + // on single submit_sm we forward it to any receiver listening to specific address range + execServiceDelReceipt.execute(new MessageForwardTask(submitSm, connectionSessionMap)); + return messageId; + } + + public void onSubmitSmRespSent(MessageId messageId, + SMPPServerSession source) { + logger.debug("submit_sm_resp with message_id {} has been sent", messageId); + } + + public SubmitMultiResult onAcceptSubmitMulti(SubmitMulti submitMulti, + SMPPServerSession source) throws ProcessRequestException { + MessageId messageId = messageIDGenerator.newMessageId(); + logger.debug("Receiving submit_multi_sm '{}', and will return message id {}", + new String(submitMulti.getShortMessage()), messageId); + if (SMSCDeliveryReceipt.SUCCESS.containedIn(submitMulti.getRegisteredDelivery()) + || SMSCDeliveryReceipt.SUCCESS_FAILURE.containedIn(submitMulti.getRegisteredDelivery())) { + execServiceDelReceipt.execute(new DeliveryReceiptTask(source, submitMulti, messageId)); + } + + return new SubmitMultiResult(messageId.getValue(), new UnsuccessDelivery[0]); + } + + public DataSmResult onAcceptDataSm(DataSm dataSm, Session source) + throws ProcessRequestException { + return null; + } + + public void onAcceptCancelSm(CancelSm cancelSm, SMPPServerSession source) + throws ProcessRequestException { + } + + public void onAcceptReplaceSm(ReplaceSm replaceSm, SMPPServerSession source) + throws ProcessRequestException { + } + + private static class WaitBindTask implements Runnable { + private final SMPPServerSession serverSession; + private final String systemId; + private final String password; + private final Map connectionSessionMap; + + public WaitBindTask(SMPPServerSession serverSession, String systemId, String password, + Map connectionSessionMap) { + this.serverSession = serverSession; + this.systemId = systemId; + this.password = password; + this.connectionSessionMap = connectionSessionMap; + } + + private void registerBindRequest(BindRequest bindRequest) { + final String range = bindRequest.getAddressRange() == null ? "" : bindRequest.getAddressRange(); + connectionSessionMap.put(serverSession, range); + logger.debug("Register bind session {} on address range {}", serverSession.getSessionId(), range); + } + + public void run() { + try { + BindRequest bindRequest = serverSession.waitForBind(1000); + try { + if(bindRequest.getSystemId().equals(systemId) && bindRequest.getPassword().equals(password)) { + bindRequest.accept(systemId); + registerBindRequest(bindRequest); + } else { + logger.error("Invalid systemId/password"); + bindRequest.reject(SMPPConstant.STAT_ESME_RINVPASWD); + } + } catch (PDUStringException e) { + logger.error("Invalid system id", e); + bindRequest.reject(SMPPConstant.STAT_ESME_RSYSERR); + } + + } catch (IllegalStateException e) { + logger.error("System error", e); + } catch (TimeoutException e) { + logger.warn("Wait for bind has reach timeout", e); + } catch (IOException e) { + logger.error("Failed accepting bind request for session {}", serverSession.getSessionId()); + } + } + } + + private static class DeliveryReceiptTask implements Runnable { + private final SMPPServerSession session; + private final MessageId messageId; + + private final TypeOfNumber sourceAddrTon; + private final NumberingPlanIndicator sourceAddrNpi; + private final String sourceAddress; + + private final TypeOfNumber destAddrTon; + private final NumberingPlanIndicator destAddrNpi; + private final String destAddress; + + private final int totalSubmitted; + private final int totalDelivered; + + private final byte[] shortMessage; + + public DeliveryReceiptTask(SMPPServerSession session, + SubmitSm submitSm, MessageId messageId) { + this.session = session; + this.messageId = messageId; + + // reversing destination to source + sourceAddrTon = TypeOfNumber.valueOf(submitSm.getDestAddrTon()); + sourceAddrNpi = NumberingPlanIndicator.valueOf(submitSm.getDestAddrNpi()); + sourceAddress = submitSm.getDestAddress(); + + // reversing source to destination + destAddrTon = TypeOfNumber.valueOf(submitSm.getSourceAddrTon()); + destAddrNpi = NumberingPlanIndicator.valueOf(submitSm.getSourceAddrNpi()); + destAddress = submitSm.getSourceAddr(); + + totalSubmitted = totalDelivered = 1; + + shortMessage = submitSm.getShortMessage(); + } + + public DeliveryReceiptTask(SMPPServerSession session, + SubmitMulti submitMulti, MessageId messageId) { + this.session = session; + this.messageId = messageId; + + // set to unknown and null, since it was submit_multi + sourceAddrTon = TypeOfNumber.UNKNOWN; + sourceAddrNpi = NumberingPlanIndicator.UNKNOWN; + sourceAddress = null; + + // reversing source to destination + destAddrTon = TypeOfNumber.valueOf(submitMulti.getSourceAddrTon()); + destAddrNpi = NumberingPlanIndicator.valueOf(submitMulti.getSourceAddrNpi()); + destAddress = submitMulti.getSourceAddr(); + + // distribution list assumed only contains single address + totalSubmitted = totalDelivered = submitMulti.getDestAddresses().length; + + shortMessage = submitMulti.getShortMessage(); + } + + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + SessionState state = session.getSessionState(); + if (!state.isReceivable()) { + logger.debug("Not sending delivery receipt for message id {} since session state is {}", + messageId, state); + return; + } + String stringValue = Integer.valueOf(messageId.getValue(), 16).toString(); + try { + + DeliveryReceipt delRec = new DeliveryReceipt(stringValue, totalSubmitted, totalDelivered, new Date(), + new Date(), DeliveryReceiptState.DELIVRD, null, new String(shortMessage)); + session.deliverShortMessage( + "mc", + sourceAddrTon, + sourceAddrNpi, + sourceAddress, + destAddrTon, + destAddrNpi, + destAddress, + new ESMClass(MessageMode.DEFAULT, MessageType.SMSC_DEL_RECEIPT, GSMSpecificFeature.DEFAULT), + (byte)0, + (byte)0, + new RegisteredDelivery(0), + DataCodings.ZERO, + delRec.toString().getBytes()); + logger.debug("Sending delivery receipt for message id " + messageId + ":" + stringValue); + } catch (Exception e) { + logger.error("Failed sending delivery_receipt for message id " + messageId + ":" + stringValue, e); + } + } + } + + private static class MessageForwardTask implements Runnable { + private final SubmitSm submitSm; + private final SMPPServerSession destination; + + public MessageForwardTask(SubmitSm submitSm, Map connectedSessionMap) { + this.submitSm = submitSm; + + // I choose possible receiver + final List possibleReceivers = new ArrayList(); + final String destAddress = submitSm.getDestAddress(); + if (destAddress != null) { + for (SMPPServerSession receiver : connectedSessionMap.keySet()) { + if (receiver.getSessionState().isReceivable()) { + // the connected session can receive and address match + if (destAddressMatch(destAddress, connectedSessionMap.get(receiver))) { + possibleReceivers.add(receiver); + } + } + } + } + if(possibleReceivers.size() > 1) + logger.warn("There are {} receivers for number {}. This may have unintended result in your test" + , possibleReceivers.size(), submitSm.getDestAddress()); + this.destination = possibleReceivers.isEmpty() ? null : possibleReceivers.get(0); + } + + private boolean destAddressMatch(String destAddress, String receiverAddressRange) { + if(receiverAddressRange.equals("")) + return false; // not listening to any address + if (destAddress.equals(receiverAddressRange)) + return true; + String[] listeningAddressRange = receiverAddressRange.split(","); + for (String addr : listeningAddressRange) { + if(destAddress.equals(addr)) + return true; + if(destAddress.matches(addr)) + return true; + } + return false; + } + + public void run() { + if (destination == null) { + // no receiver listening for message, so nothing to do here + return; + } + + try { + final byte[] message = submitSm.getShortMessage(); + logger.debug("Forwards incoming message {} to session {}. from {} to {}", + new String[] {new String(message), destination.getSessionId(), + submitSm.getSourceAddr(), submitSm.getDestAddress()}); + destination.deliverShortMessage("mc", + TypeOfNumber.valueOf(submitSm.getSourceAddrTon()), + NumberingPlanIndicator.valueOf(submitSm.getSourceAddrNpi()), + submitSm.getSourceAddr(), + TypeOfNumber.valueOf(submitSm.getDestAddrTon()), + NumberingPlanIndicator.valueOf(submitSm.getDestAddrNpi()), + submitSm.getDestAddress(), + new ESMClass(MessageMode.DEFAULT, MessageType.DEFAULT, GSMSpecificFeature.DEFAULT), + (byte) 0, + (byte) 0, + new RegisteredDelivery(0), + DataCodings.ZERO, + message); + } catch (Exception e) { + logger.error("Fail forwarding message to consumer: " + destination.getSessionId(), e); + } + } + } + + @PostConstruct + public void onPostConstruct() { + logger.debug("Starting mock SMPP server"); + execService.submit(this); + } + + @PreDestroy + public void onDestroy() { + logger.debug("Destroying mock SMPP server"); + connectionSessionMap.clear(); + } +} diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppInboundGateway.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppInboundGateway.java index 15dd8cc..58b76d7 100644 --- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppInboundGateway.java +++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppInboundGateway.java @@ -4,8 +4,11 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.integration.Message; +import org.springframework.integration.MessagingException; import org.springframework.integration.core.MessageHandler; import org.springframework.integration.core.SubscribableChannel; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; @@ -21,13 +24,15 @@ import java.util.concurrent.atomic.AtomicInteger; @RunWith(SpringJUnit4ClassRunner.class) public class TestSmppInboundGateway { + private final Logger logger = LoggerFactory.getLogger(TestSmppInboundGateway.class); + @Value("#{out1}") SubscribableChannel out1; @Value("#{out2}") SubscribableChannel out2; @Value("#{in1}") SubscribableChannel in1; @Value("#{in2}") SubscribableChannel in2; // test data - String toPhone = "33333"; // todo make sure the gateway automatically 'flips' with the to/from on the reply SMS + String toPhone = "33333"; String fromPhone = "1111"; long now = System.currentTimeMillis(); String smsRequest = "this is a request created at " + now; @@ -61,11 +66,11 @@ public class TestSmppInboundGateway { // anyway.... //1) lets send an outbound message so that our gateway has something to listen for - SmesMessageSpecification.newSmesMessageSpecification(outSession, this.fromPhone, this.toPhone, this.smsRequest).send(); MessageHandler inboundMessageHandler = new AbstractReplyProducingMessageHandler() { @Override protected Object handleRequestMessage(Message requestMessage) { + logger.debug("Processing incoming message for inbound-gw and produce a reply"); Assert.assertEquals(requestMessage.getPayload(), smsRequest); count.incrementAndGet(); return MessageBuilder.withPayload( @@ -75,7 +80,25 @@ public class TestSmppInboundGateway { }; this.in1.subscribe(inboundMessageHandler); - // launch the whole thing + // this is handler for reply from inbound-gateway + MessageHandler replyHandler = new MessageHandler() { + @Override + public void handleMessage(Message message) throws MessagingException { + logger.debug("Reply handler receives: "+message.getPayload()); + Assert.assertEquals(message.getPayload(), smsResponse); + } + }; + this.in2.subscribe(replyHandler); + + // outbound gateway send + logger.debug("Sending message from: {} to: {} message: '{}'", new String[] {fromPhone, toPhone, smsRequest}); + SmesMessageSpecification.newSmesMessageSpecification(outSession, this.fromPhone, this.toPhone, this.smsRequest).send(); + /*Message smsMsg = MessageBuilder.withPayload(smsRequest) + .setHeader(SmppConstants.SRC_ADDR, fromPhone) + .setHeader(SmppConstants.DST_ADDR, toPhone) + .setHeader(SmppConstants.REGISTERED_DELIVERY_MODE, SMSCDeliveryReceipt.SUCCESS) + .build(); + out2.send(smsMsg);*/ Thread.sleep(1000 * 10); diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionFactoryBean.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionFactoryBean.java index 13f1357..a0a70d2 100644 --- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionFactoryBean.java +++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/TestSmppSessionFactoryBean.java @@ -16,6 +16,7 @@ import org.junit.*; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.integration.smpp.session.DelegatingMessageReceiverListener; import org.springframework.integration.smpp.session.ExtendedSmppSession; import org.springframework.integration.smpp.session.ExtendedSmppSessionAdaptingDelegate; @@ -47,13 +48,17 @@ public class TestSmppSessionFactoryBean { private AbsoluteTimeFormatter timeFormatter = new AbsoluteTimeFormatter(); - private String host = "127.0.0.1"; + @Value("${smpp.host}") + private String host; - private int port = 2775; + @Value("${smpp.port}") + private int port; - private String systemId = "smppclient1"; + @Value("${smpp.systemId}") + private String systemId; - private String password = "password"; + @Value("${smpp.password}") + private String password; @Test public void testSmppSessionFactory() throws Throwable { diff --git a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppInboundGatewayParserTests.java b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppInboundGatewayParserTests.java index 9ac2ac5..4aec6fd 100644 --- a/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppInboundGatewayParserTests.java +++ b/spring-integration-smpp/src/test/java/org/springframework/integration/smpp/config/xml/SmppInboundGatewayParserTests.java @@ -70,9 +70,9 @@ public class SmppInboundGatewayParserTests { assertEquals("errorChannel", errorChannel.getComponentName()); // mappers - InboundMessageMapper inboundMessageMapper = TestUtils.getPropertyValue(gateway, "requestMapper", InboundMessageMapper.class); + InboundMessageMapper inboundMessageMapper = TestUtils.getPropertyValue(gateway, "requestMapper", InboundMessageMapper.class); assertNotNull(inboundMessageMapper); - OutboundMessageMapper outboundMessageMapper = TestUtils.getPropertyValue(gateway, "messageConverter.outboundMessageMapper", OutboundMessageMapper.class); + OutboundMessageMapper outboundMessageMapper = TestUtils.getPropertyValue(gateway, "messageConverter.outboundMessageMapper", OutboundMessageMapper.class); assertNotNull(outboundMessageMapper); } diff --git a/spring-integration-smpp/src/test/resources/TestSmppConnection-context.xml b/spring-integration-smpp/src/test/resources/TestSmppConnection-context.xml index 6211653..08e9f9b 100644 --- a/spring-integration-smpp/src/test/resources/TestSmppConnection-context.xml +++ b/spring-integration-smpp/src/test/resources/TestSmppConnection-context.xml @@ -11,6 +11,12 @@ + + + + + + diff --git a/spring-integration-smpp/src/test/resources/TestSmppInboundChannelAdapter-context.xml b/spring-integration-smpp/src/test/resources/TestSmppInboundChannelAdapter-context.xml index 5e4cbac..b9d5b00 100644 --- a/spring-integration-smpp/src/test/resources/TestSmppInboundChannelAdapter-context.xml +++ b/spring-integration-smpp/src/test/resources/TestSmppInboundChannelAdapter-context.xml @@ -9,6 +9,13 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + + + + + + + diff --git a/spring-integration-smpp/src/test/resources/TestSmppInboundGateway-context.xml b/spring-integration-smpp/src/test/resources/TestSmppInboundGateway-context.xml index 393c146..2cb7bb6 100644 --- a/spring-integration-smpp/src/test/resources/TestSmppInboundGateway-context.xml +++ b/spring-integration-smpp/src/test/resources/TestSmppInboundGateway-context.xml @@ -8,6 +8,13 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + + + + + + + @@ -17,7 +24,7 @@ - + @@ -39,17 +46,24 @@ - + + + + + - + + + diff --git a/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapter-context.xml b/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapter-context.xml index 9377679..cb9c79f 100644 --- a/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapter-context.xml +++ b/spring-integration-smpp/src/test/resources/TestSmppOutboundChannelAdapter-context.xml @@ -8,6 +8,13 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + + + + + + + + + + + + + + + + + + + +