This is an automated email from the ASF dual-hosted git repository. solomax pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/openmeetings.git
The following commit(s) were added to refs/heads/master by this push: new c44043a [OPENMEETINGS-2239] initial commit, basic work with jain-sip c44043a is described below commit c44043a6d3a1dc34844d1ddd580c0569cb47adbf Author: Maxim Solodovnik <solomax...@gmail.com> AuthorDate: Mon Oct 12 13:31:39 2020 +0700 [OPENMEETINGS-2239] initial commit, basic work with jain-sip --- openmeetings-db/pom.xml | 4 + .../apache/openmeetings/db/dao/room/SipConfig.java | 123 ++++++++ .../apache/openmeetings/db/dao/room/SipDao.java | 339 +++++++++++++++++++-- .../src/site/markdown/AsteriskIntegration.md | 107 +++++-- openmeetings-web/pom.xml | 2 +- .../webapp/WEB-INF/classes/applicationContext.xml | 24 +- 6 files changed, 538 insertions(+), 61 deletions(-) diff --git a/openmeetings-db/pom.xml b/openmeetings-db/pom.xml index ef733ec..cd6d82a 100644 --- a/openmeetings-db/pom.xml +++ b/openmeetings-db/pom.xml @@ -109,6 +109,10 @@ <version>${mssql.version}</version> </dependency> <dependency> + <groupId>javax.sip</groupId> + <artifactId>jain-sip-ri</artifactId> + </dependency> + <dependency> <groupId>org.apache.openmeetings</groupId> <artifactId>openmeetings-util</artifactId> <version>${project.version}</version> diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java new file mode 100644 index 0000000..e1a5646 --- /dev/null +++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License") + you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openmeetings.db.dao.room; + +public class SipConfig { + private String sipHostname; + private int managerPort; + private String managerUser; + private String managerPass; + private long managerTimeout; + + private int localWsPort = 6666; + private String localWsHost; + private int wsPort; + private String omSipUser; + private String omSipPasswd; + + private String uid; //FIXME TODO is this still required ?! + + public String getSipHostname() { + return sipHostname; + } + + public void setSipHostname(String sipHostname) { + this.sipHostname = sipHostname; + } + + public int getManagerPort() { + return managerPort; + } + + public void setManagerPort(int managerPort) { + this.managerPort = managerPort; + } + + public String getManagerUser() { + return managerUser; + } + + public void setManagerUser(String managerUser) { + this.managerUser = managerUser; + } + + public String getManagerPass() { + return managerPass; + } + + public void setManagerPass(String managerPass) { + this.managerPass = managerPass; + } + + public long getManagerTimeout() { + return managerTimeout; + } + + public void setManagerTimeout(long managerTimeout) { + this.managerTimeout = managerTimeout; + } + + public int getLocalWsPort() { + return localWsPort; + } + + public void setLocalWsPort(int localWsPort) { + this.localWsPort = localWsPort; + } + + public String getLocalWsHost() { + return localWsHost; + } + + public void setLocalWsHost(String localWsHost) { + this.localWsHost = localWsHost; + } + + public int getWsPort() { + return wsPort; + } + + public void setWsPort(int wsPort) { + this.wsPort = wsPort; + } + + public String getOmSipUser() { + return omSipUser; + } + + public void setOmSipUser(String omSipUser) { + this.omSipUser = omSipUser; + } + + public String getOmSipPasswd() { + return omSipPasswd; + } + + public void setOmSipPasswd(String omSipPasswd) { + this.omSipPasswd = omSipPasswd; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } +} diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java index 900ce29..c41fdc6 100644 --- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java +++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java @@ -18,6 +18,41 @@ */ package org.apache.openmeetings.db.dao.room; +import static javax.sip.message.Request.INVITE; +import static javax.sip.message.Request.REGISTER; +import static javax.sip.message.Response.OK; +import static javax.sip.message.Response.RINGING; +import static javax.sip.message.Response.TRYING; +import static javax.sip.message.Response.UNAUTHORIZED; + +import java.text.ParseException; +import java.util.List; +import java.util.Properties; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import javax.annotation.PostConstruct; +import javax.sip.ClientTransaction; +import javax.sip.DialogTerminatedEvent; +import javax.sip.IOExceptionEvent; +import javax.sip.ListeningPoint; +import javax.sip.RequestEvent; +import javax.sip.ResponseEvent; +import javax.sip.ServerTransaction; +import javax.sip.SipException; +import javax.sip.SipFactory; +import javax.sip.SipProvider; +import javax.sip.TimeoutEvent; +import javax.sip.TransactionTerminatedEvent; +import javax.sip.address.Address; +import javax.sip.address.AddressFactory; +import javax.sip.header.ContactHeader; +import javax.sip.header.HeaderFactory; +import javax.sip.message.MessageFactory; +import javax.sip.message.Request; +import javax.sip.message.Response; + import org.apache.openmeetings.db.entity.room.Room; import org.asteriskjava.manager.DefaultManagerConnection; import org.asteriskjava.manager.ManagerConnection; @@ -35,40 +70,94 @@ import org.asteriskjava.manager.response.ManagerError; import org.asteriskjava.manager.response.ManagerResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import gov.nist.javax.sip.DialogTimeoutEvent; +import gov.nist.javax.sip.SipListenerExt; +import gov.nist.javax.sip.SipStackExt; +import gov.nist.javax.sip.address.SipUri; +import gov.nist.javax.sip.clientauthutils.AuthenticationHelper; +import gov.nist.javax.sip.clientauthutils.UserCredentials; +import gov.nist.javax.sip.stack.NioMessageProcessorFactory; -public class SipDao { +@Service +public class SipDao implements SipListenerExt { private static final Logger log = LoggerFactory.getLogger(SipDao.class); public static final String ASTERISK_OM_FAMILY = "openmeetings"; public static final String ASTERISK_OM_KEY = "rooms"; public static final String SIP_FIRST_NAME = "SIP Transport"; public static final String SIP_USER_NAME = "--SIP--"; - private String sipHostname; - private int sipPort; - private String sipUsername; - private String sipPassword; - private String uid; - private long timeout; + private static final String SIP_TRANSPORT = "ws"; + private static final <T> Consumer<T> NOOP() { + return t -> {}; + } + + private final AtomicLong cseq = new AtomicLong(); + private final Random rnd = new Random(); + + private String tag; + private String branch; + + private SipProvider sipProvider; + private SipFactory sipFactory; + private SipStackExt sipStack; + private MessageFactory messageFactory; + private HeaderFactory headerFactory; + private AddressFactory addressFactory; + private ContactHeader contactHeader; private ManagerConnectionFactory factory; - public SipDao() { - // enabled for @SpringBean - } + @Autowired + private SipConfig config; + + @PostConstruct + public void init() throws Exception { + if (config.getSipHostname() != null) { + factory = new ManagerConnectionFactory( + config.getSipHostname() + , config.getManagerPort() + , config.getManagerUser() + , config.getManagerPass()); + sipFactory = SipFactory.getInstance(); + sipFactory.setPathName("gov.nist"); - public SipDao(String sipHostname, int sipPort, String sipUsername, String sipPassword, long timeout) { - this.sipHostname = sipHostname; - this.sipPort = sipPort; - this.sipUsername = sipUsername; - this.sipPassword = sipPassword; - this.timeout = timeout; - factory = new ManagerConnectionFactory(this.sipHostname, this.sipPort, this.sipUsername, this.sipPassword); + final Properties properties = new Properties(); + properties.setProperty("javax.sip.STACK_NAME", "stack"); + //properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "32"); + properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "true"); + properties.setProperty("gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", NioMessageProcessorFactory.class.getName()); + sipStack = (SipStackExt) sipFactory.createSipStack(properties); + tag = getRnd(10); + branch = getRnd(14); + + messageFactory = sipFactory.createMessageFactory(); + headerFactory = sipFactory.createHeaderFactory(); + addressFactory = sipFactory.createAddressFactory(); + final ListeningPoint listeningPoint = sipStack.createListeningPoint( + config.getLocalWsHost() + , config.getLocalWsPort() + , SIP_TRANSPORT); + sipProvider = sipStack.createSipProvider(listeningPoint); + sipProvider.addSipListener(this); + Address contact = createAddr(config.getOmSipUser(), config.getLocalWsHost(), uri -> { + try { + uri.setPort(config.getLocalWsPort()); + uri.setTransportParam(SIP_TRANSPORT); + } catch (ParseException e) { + log.error("fail to create contact address", e); + } + }); + contactHeader = headerFactory.createContactHeader(contact); + } } private ManagerConnection getConnection() { DefaultManagerConnection con = (DefaultManagerConnection)factory.createManagerConnection(); - con.setDefaultEventTimeout(timeout); - con.setDefaultResponseTimeout(timeout); - con.setSocketReadTimeout((int)timeout); - con.setSocketTimeout((int)timeout); + con.setDefaultEventTimeout(config.getManagerTimeout()); + con.setDefaultResponseTimeout(config.getManagerTimeout()); + con.setSocketReadTimeout((int)config.getManagerTimeout()); + con.setSocketTimeout((int)config.getManagerTimeout()); return con; } @@ -126,6 +215,10 @@ public class SipDao { return ASTERISK_OM_KEY + "/" + confno; } + private static String getSipNumber(Room r) { + return (r != null && r.getConfno() != null) ? r.getConfno() : null; + } + public String get(String confno) { String pin = null; DbGetAction da = new DbGetAction(ASTERISK_OM_FAMILY, getKey(confno)); @@ -175,7 +268,7 @@ public class SipDao { * room to be connected to the call */ public void joinToConfCall(String number, Room r) { - String sipNumber = (r != null && r.getConfno() != null) ? r.getConfno() : null; + String sipNumber = getSipNumber(r); if (sipNumber == null) { log.warn("Failed to get SIP number for room: {}", r); return; @@ -186,16 +279,208 @@ public class SipDao { oa.setContext("rooms-out"); oa.setExten(number); oa.setPriority(1); - oa.setTimeout(timeout); + oa.setTimeout(config.getManagerTimeout()); exec(oa); } - public String getUid() { - return uid; + @Override + public void processDialogTerminated(DialogTerminatedEvent evt) { + log.error("processDialogTerminated: \n{}", evt); + } + + @Override + public void processIOException(IOExceptionEvent evt) { + log.error("processIOException: \n{}", evt); } - public void setUid(String uid) { - this.uid = uid; + @Override + public void processTimeout(TimeoutEvent evt) { + log.error("processTimeout: \n{}", evt); + } + + @Override + public void processTransactionTerminated(TransactionTerminatedEvent evt) { + log.error("processTransactionTerminated: \n{}", evt); + } + + @Override + public void processDialogTimeout(DialogTimeoutEvent timeoutEvent) { + log.error("processDialogTimeout: \n{}", timeoutEvent); + } + + @Override + public void processRequest(RequestEvent evt) { + log.debug("processRequest: \n\n{}", evt.getRequest()); + Request rq = evt.getRequest(); + String method = rq.getMethod(); + try { + if (Request.OPTIONS.equals(method)) { + ServerTransaction transaction = sipProvider.getNewServerTransaction(rq); + Response resp = messageFactory.createResponse(200, rq); + resp.addHeader(contactHeader); + transaction.sendResponse(resp); + } + } catch (Exception e) { + log.error("processRequest", e); + } + } + + @Override + public void processResponse(ResponseEvent evt) { + Response resp = evt.getResponse(); + ClientTransaction curTrans = evt.getClientTransaction(); + Request prevReq = curTrans.getRequest(); + log.warn("Response code: {} on {}", resp.getStatusCode(), prevReq.getMethod()); + switch (resp.getStatusCode()) { + case UNAUTHORIZED: + processUnauth(evt); + break; + case OK: + break; + case TRYING: + case RINGING: + break; + // FIXME TODO other codes: 404 + default: + log.debug("No handler for response: \n\n{}", resp); + } + } + + private String getRnd(int count) { + return rnd.ints('0', 'z' + 1) + .filter(ch -> (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')) + .limit(count) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + + private Address createAddr(String user) { + return createAddr(user, config.getSipHostname(), NOOP()); + } + + private Address createAddr(String user, String host, Consumer<SipUri> cons) { + try { + SipUri uri = new SipUri(); + uri.setHost(host); + uri.setUser(user); + cons.accept(uri); + return addressFactory.createAddress(user, uri); + } catch (ParseException e) { + log.error("fail to create address", e); + } + return null; + } + + private void sendRequest(String method, String to, Consumer<SipUri> uriCons, Consumer<Request> reqCons) throws Exception { + SipUri uri = new SipUri(); + uri.setHost(config.getSipHostname()); + uri.setPort(config.getWsPort()); + uri.setTransportParam(SIP_TRANSPORT); + uri.setMethodParam("GET"); + uri.setHeader("Host", config.getSipHostname()); + uri.setHeader("Location", "/ws"); + uriCons.accept(uri); + + Request request = messageFactory.createRequest( + uri + , method + , sipProvider.getNewCallId() + , headerFactory.createCSeqHeader(cseq.incrementAndGet(), method) + , headerFactory.createFromHeader(createAddr(config.getOmSipUser()), tag) + , headerFactory.createToHeader(createAddr(to), null) + , List.of(headerFactory.createViaHeader(config.getLocalWsHost(), config.getLocalWsPort(), SIP_TRANSPORT, branch)) + , headerFactory.createMaxForwardsHeader(70)); + request.addHeader(contactHeader); + request.addHeader(headerFactory.createExpiresHeader(600)); + + reqCons.accept(request); + + log.debug("sendRequest: \n\n{}", request); + + ClientTransaction trans = sipProvider.getNewClientTransaction(request); + trans.sendRequest(); + } + + private void processUnauth(ResponseEvent evt) { + Response resp = evt.getResponse(); + ClientTransaction curTrans = evt.getClientTransaction(); + AuthenticationHelper helper = sipStack.getAuthenticationHelper((trans, s) -> new UserCredentials() { + @Override + public String getUserName() { + return config.getOmSipUser(); + } + + @Override + public String getPassword() { + return config.getOmSipPasswd(); + } + + @Override + public String getSipDomain() { + return "asterisk"; + } + }, headerFactory); + try { + ClientTransaction trans = helper.handleChallenge(resp, curTrans, sipProvider, 5); + trans.sendRequest(); + } catch (SipException e) { + log.error("Error while sending AUTH", e); + } + } + + private void addAllow(Request req) throws ParseException { + req.addHeader(headerFactory.createAllowHeader("ACK,CANCEL,INVITE,MESSAGE,BYE,OPTIONS,INFO,NOTIFY,REFER")); + } + + private void register() throws Exception { + sendRequest( + REGISTER + , config.getOmSipUser() + , NOOP() + , req -> { + try { + addAllow(req); + } catch (ParseException e) { + log.error("fail to create allow header", e); + } + }); + } + + private void invite(Room r, String sdp) throws Exception { + final String sipNumber = getSipNumber(r); + if (sipNumber == null) { + log.warn("Failed to get SIP number for room: {}", r); + return; + } + sendRequest( + INVITE + , sipNumber + , uri -> uri.setUser(sipNumber) + , req -> { + /* + * ContentLengthHeader contentLength = + * this.headerFactory.createContentLengthHeader(300); + * ContentTypeHeader contentType = + * this.headerFactory.createContentTypeHeader("application", + * "sdp"); + * + * String sdpData = "v=0\n" + + * "o=user1 392867480 292042336 IN IP4 xx.xx.xx.xx\n" + "s=-\n" + * + "c=IN IP4 xx.xx.xx.xx\n" + "t=0 0\n" + + * "m=audio 8000 RTP/AVP 0 8 101\n" + "a=rtpmap:0 PCMU/8000\n" + + * "a=rtpmap:8 PCMA/8000\n" + + * "a=rtpmap:101 telephone-event/8000\n" + "a=sendrecv"; byte[] + * contents = sdpData.getBytes(); this.contactHeader = + * this.headerFactory.createContactHeader(contactAddress); + */ + try { + addAllow(req); + req.addHeader(headerFactory.createContentLengthHeader(sdp.length())); + req.setContent(sdp, headerFactory.createContentTypeHeader("application", "sdp")); + } catch (Exception e) { + log.error("fail to create allow header", e); + } + }); } } diff --git a/openmeetings-server/src/site/markdown/AsteriskIntegration.md b/openmeetings-server/src/site/markdown/AsteriskIntegration.md index 44107ce..2fd9105 100644 --- a/openmeetings-server/src/site/markdown/AsteriskIntegration.md +++ b/openmeetings-server/src/site/markdown/AsteriskIntegration.md @@ -125,19 +125,22 @@ rtcachefriends=yes maxexpiry=43200 ``` -** FIXME TODO Add user for the "SIP Transport"** +**Add user for the "SIP Transport"** ``` -; FIXME TODO commented for now -; [red5sip_user] -; type=friend -; secret=12345 -; disallow=all -; allow=ulaw -; allow=h263 -; host=dynamic -; nat=force_rport,comedia -; context=rooms-red5sip +[omsip_user] +host=dynamic +secret=12345 +context=rooms-omsip +transport=ws,wss +type=friend +encryption=no +avpf=yes +icesupport=yes +directmedia=no +disallow=all +allow=ulaw,opus +allow=vp8 ``` ### Configure extensions: @@ -187,11 +190,10 @@ exten => _400X!,n,Hangup ; Extensions for outgoing calls from Openmeetings room. ; ***************************************************** -; FIXME TODO commented for now -;[rooms-red5sip] -;exten => _400X!,1,GotoIf($[${DB_EXISTS(openmeetings/rooms/${EXTEN})}]?ok:notavail) -;exten => _400X!,n(ok),Confbridge(${EXTEN},default_bridge,red5sip_user) -;exten => _400X!,n(notavail),Hangup +[rooms-omsip] +exten => _400X!,1,GotoIf($[${DB_EXISTS(openmeetings/rooms/${EXTEN})}]?ok:notavail) +exten => _400X!,n(ok),Confbridge(${EXTEN},default_bridge,omsip_user) +exten => _400X!,n(notavail),Hangup ``` ### Configure Confbridge @@ -203,12 +205,11 @@ Modify `/etc/asterisk/confbridge.conf` ``` [general] -; FIXME TODO commented for now -;[red5sip_user] -;type=user -;marked=yes -;dsp_drop_silence=yes -;denoise=true +[omsip_user] +type=user +marked=yes +dsp_drop_silence=yes +denoise=true [sip_user] type=user @@ -247,8 +248,8 @@ write = all Update OpenMeetings with credentials for Asterisk manager. Modify `/opt/om/webapps/openmeetings/WEB-INF/classes/applicationContext.xml` -find **<bean id="sipDao" class="org.apache.openmeetings.db.dao.room.SipDao">** -uncomment its parameters and set it to your custom values. +find **<bean class="org.apache.openmeetings.db.dao.room.SipConfig">** +uncomment its properties and set it to your custom values. set value for `uid` property to unique secret value (can be generated here <a href="https://www.uuidtools.com">https://www.uuidtools.com</a>) @@ -257,3 +258,61 @@ set value for `uid` property to unique secret value (can be generated here <a hr otherwise all SIP related room information will be lost </p> +### Configure Asterisk's built-in HTTP server + +To communicate with WebSocket clients, Asterisk uses its built-in HTTP server. Configure `/etc/asterisk/http.conf` as follows: + +``` +[general] +enabled=yes +bindaddr=127.0.0.1 ; or your Asterisk IP +bindport=8088 +tlsenable=yes +tlsbindaddr=0.0.0.0:8089 +tlscertfile=/etc/asterisk/keys/asterisk.crt +tlsprivatekey=/etc/asterisk/keys/asterisk.key +``` + +### Configure PJSIP + +If you're not already familiar with configuring Asterisk's chan_pjsip driver, visit the res_pjsip configuration page. + +Modify `/etc/asterisk/pjsip.conf` as follows: + +``` +[transport-wss] +type=transport +protocol=wss +bind=0.0.0.0 +; All other transport parameters are ignored for wss transports. + +[webrtc_client] +type=aor +max_contacts=5 +remove_existing=yes + +[webrtc_client] +type=auth +auth_type=userpass +username=webrtc_client +password=webrtc_client ; This is a completely insecure password! Do NOT expose this + ; system to the Internet without utilizing a better password. + +[webrtc_client] +type=endpoint +aors=webrtc_client +auth=webrtc_client +dtls_auto_generate_cert=yes +webrtc=yes +; Setting webrtc=yes is a shortcut for setting the following options: +; use_avpf=yes +; media_encryption=dtls +; dtls_verify=fingerprint +; dtls_setup=actpass +; ice_support=yes +; media_use_received_transport=yes +; rtcp_mux=yes +context=default +disallow=all +allow=opus,ulaw +``` diff --git a/openmeetings-web/pom.xml b/openmeetings-web/pom.xml index 8734b46..c9a6fd1 100644 --- a/openmeetings-web/pom.xml +++ b/openmeetings-web/pom.xml @@ -663,7 +663,7 @@ <dependency> <groupId>org.webjars</groupId> <artifactId>font-awesome</artifactId> - <version>5.14.0</version> + <version>${font-awesome.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> diff --git a/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml b/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml index 567814b..0edbe2b 100644 --- a/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml +++ b/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml @@ -124,16 +124,22 @@ </bean> <!-- End of Services --> - <!-- Interface Transactional --> - <bean id="sipDao" class="org.apache.openmeetings.db.dao.room.SipDao"> - <!-- Should be uncommented and updated with real values for Asterisk - <constructor-arg index="0" value="127.0.0.1"/> - <constructor-arg index="1" value="5038" type = "int"/> - <constructor-arg index="2" value="openmeetings"/> - <constructor-arg index="3" value="12345"/> - <constructor-arg index="4" value="10000" type = "long"/> + <bean class="org.apache.openmeetings.db.dao.room.SipConfig"> + <!-- Should be uncommented and updated with real values for Asterisk- + <property name="sipHostname" value="192.168.1.102"/> + <property name="managerPort" value="5038"/> + <property name="managerUser" value="openmeetings"/> + <property name="managerPass" value="12345"/> + <property name="managerTimeout" value="10000"/> + + <property name="localWsPort" value="6666"/> + <property name="localWsHost" value="192.168.1.211"/> + <property name="wsPort" value="8088"/> + <property name="omSipUser" value="omsip_user"/> + <property name="omSipPasswd" value="12345"/> + <property name="uid" value="87dddad4-9ca5-475b-860f-2e0825d02b76"/> - --> + - --> </bean> <!-- Thread Executor -->