Makefile.fetch                                                           |    
1 
 RepositoryExternal.mk                                                    |   
11 
 configure.ac                                                             |   
11 
 desktop/qa/desktop_lib/test_desktop_lib.cxx                              |    
4 
 desktop/source/lib/init.cxx                                              |  
227 +++
 download.lst                                                             |    
5 
 external/Module_external.mk                                              |    
1 
 external/java_websocket/ExternalPackage_java_websocket.mk                |   
16 
 external/java_websocket/ExternalProject_java_websocket.mk                |   
31 
 external/java_websocket/Makefile                                         |    
7 
 external/java_websocket/Module_java_websocket.mk                         |   
18 
 external/java_websocket/README                                           |    
3 
 external/java_websocket/UnpackedTarball_java_websocket.mk                |   
21 
 external/java_websocket/patches/ant-build.patch                          |   
26 
 external/java_websocket/patches/no-slf4j.patch                           |  
748 ++++++++++
 include/LibreOfficeKit/LibreOfficeKit.h                                  |    
9 
 include/LibreOfficeKit/LibreOfficeKit.hxx                                |   
26 
 include/sal/log-areas.dox                                                |    
1 
 readlicense_oo/license/license.xml                                       |   
18 
 ridljar/Jar_libreoffice.mk                                               |    
8 
 ridljar/com/sun/star/comp/helper/Bootstrap.java                          |   
63 
 ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java |   
60 
 ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java  |  
335 ++++
 ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java   |  
137 +
 ridljar/source/libreoffice/module-info.java                              |    
1 
 ridljar/util/manifest                                                    |    
3 
 ure/source/README                                                        |    
2 
 27 files changed, 1792 insertions(+), 1 deletion(-)

New commits:
commit 8e246331f6f71320cfcc8defdd04e756a75f71cf
Author:     Skyler Grey <skyler.g...@collabora.com>
AuthorDate: Fri Aug 18 13:30:35 2023 +0000
Commit:     Caolán McNamara <caolan.mcnam...@collabora.com>
CommitDate: Fri Sep 1 16:55:37 2023 +0200

    Add a FunctionBasedURPConnection and a websocket URP connector
    
    - FunctionBasedURPConnection is used to enable a client to open a URP
      connection to a fresh Kit instance in COOL.
    - This URP connector can be used with that and
      https://github.com/CollaboraOnline/online/pull/6992 to use a Java Uno
      Remote Protocol client over websockets
    - For interoperability with existing Collabora Online websockets a
      prefix (urp ) is added to each message sent and a similar prefix
      (urp: ) is expected on each message recieved. This allows sending over
      the same websocket as other data is being transmitted through. If you
      are writing a bridge to work with this, you will need to add/strip the
      prefixes accordingly
    - This commit uses Java WebSocket
      (https://github.com/TooTallNate/Java-WebSocket) to send data over
      websockets.
    
    Change-Id: I2bda3d0b988bef7883f9b6829eeb5b7ae8075f27
    Signed-off-by: Skyler Grey <skyler.g...@collabora.com>
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/151171
    Tested-by: Jenkins
    Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com>

diff --git a/Makefile.fetch b/Makefile.fetch
index 625e781dc36a..6640b0ade985 100644
--- a/Makefile.fetch
+++ b/Makefile.fetch
@@ -136,6 +136,7 @@ $(WORKDIR)/download: $(BUILDDIR)/config_$(gb_Side).mk 
$(SRCDIR)/download.lst $(S
                $(call fetch_Optional,HYPHEN,HYPHEN_TARBALL) \
                $(call fetch_Optional,ICU,ICU_TARBALL) \
                $(call fetch_Optional,ICU,ICU_DATA_TARBALL) \
+               $(call fetch_Optional,JAVA_WEBSOCKET,JAVA_WEBSOCKET_TARBALL) \
                $(call 
fetch_Optional,JFREEREPORT,JFREEREPORT_FLOW_ENGINE_TARBALL) \
                $(call fetch_Optional,JFREEREPORT,JFREEREPORT_FLUTE_TARBALL) \
                $(call fetch_Optional,JFREEREPORT,JFREEREPORT_LIBBASE_TARBALL) \
diff --git a/RepositoryExternal.mk b/RepositoryExternal.mk
index 0d70c7e966fb..a7f6dc3897ec 100644
--- a/RepositoryExternal.mk
+++ b/RepositoryExternal.mk
@@ -3915,6 +3915,17 @@ endef
 
 endif # SYSTEM_JFREEREPORT
 
+# no known distro packaged Java-Websocket at present
+
+ifeq ($(ENABLE_JAVA),TRUE)
+$(eval $(call gb_Helper_register_jars_for_install,URE,ure, \
+       java_websocket \
+))
+endif
+
+define gb_Jar__use_java_websocket
+$(call gb_Jar_use_jar,$(1),java_websocket)
+endef
 
 # Executables
 
diff --git a/configure.ac b/configure.ac
index 5ce8454546c4..5a682dde4d61 100644
--- a/configure.ac
+++ b/configure.ac
@@ -12690,6 +12690,17 @@ AC_SUBST(LIBASSUAN_LIBS)
 AC_SUBST(GPGMEPP_CFLAGS)
 AC_SUBST(GPGMEPP_LIBS)
 
+AC_MSG_CHECKING([whether to build Java Websocket for the UNO remote websocket 
client])
+if test "$with_java" != "no"; then
+    AC_MSG_RESULT([yes])
+    ENABLE_JAVA_WEBSOCKET=TRUE
+    BUILD_TYPE="$BUILD_TYPE JAVA_WEBSOCKET"
+else
+    AC_MSG_RESULT([no])
+    ENABLE_JAVA_WEBSOCKET=
+fi
+AC_SUBST(ENABLE_JAVA_WEBSOCKET)
+
 AC_MSG_CHECKING([whether to build the Wiki Publisher extension])
 if test "x$enable_ext_wiki_publisher" = "xyes" -a 
"x$enable_extension_integration" != "xno" -a "$with_java" != "no"; then
     AC_MSG_RESULT([yes])
diff --git a/desktop/qa/desktop_lib/test_desktop_lib.cxx 
b/desktop/qa/desktop_lib/test_desktop_lib.cxx
index 35cbbfe60c15..8a55ae18172d 100644
--- a/desktop/qa/desktop_lib/test_desktop_lib.cxx
+++ b/desktop/qa/desktop_lib/test_desktop_lib.cxx
@@ -3681,10 +3681,12 @@ void DesktopLOKTest::testABI()
     CPPUNIT_ASSERT_EQUAL(classOffset(15), offsetof(struct 
_LibreOfficeKitClass, dumpState));
     CPPUNIT_ASSERT_EQUAL(classOffset(16), offsetof(struct 
_LibreOfficeKitClass, extractRequest));
     CPPUNIT_ASSERT_EQUAL(classOffset(17), offsetof(struct 
_LibreOfficeKitClass, trimMemory));
+    CPPUNIT_ASSERT_EQUAL(classOffset(18), offsetof(struct 
_LibreOfficeKitClass, startURP));
+    CPPUNIT_ASSERT_EQUAL(classOffset(19), offsetof(struct 
_LibreOfficeKitClass, stopURP));
 
     // When extending LibreOfficeKit with a new function pointer,  add new 
assert for the offsetof the
     // new function pointer and bump this assert for the size of the class.
-    CPPUNIT_ASSERT_EQUAL(classOffset(18), sizeof(struct _LibreOfficeKitClass));
+    CPPUNIT_ASSERT_EQUAL(classOffset(20), sizeof(struct _LibreOfficeKitClass));
 
     CPPUNIT_ASSERT_EQUAL(documentClassOffset(0), offsetof(struct 
_LibreOfficeKitDocumentClass, destroy));
     CPPUNIT_ASSERT_EQUAL(documentClassOffset(1), offsetof(struct 
_LibreOfficeKitDocumentClass, saveAs));
diff --git a/desktop/source/lib/init.cxx b/desktop/source/lib/init.cxx
index 6db6f722bee6..4b81619cbc1f 100644
--- a/desktop/source/lib/init.cxx
+++ b/desktop/source/lib/init.cxx
@@ -49,6 +49,7 @@
 #include <memory>
 #include <iostream>
 #include <string_view>
+#include <queue>
 
 #include <boost/property_tree/json_parser.hpp>
 #include <boost/algorithm/string.hpp>
@@ -86,6 +87,7 @@
 #include <comphelper/servicehelper.hxx>
 #include <comphelper/sequenceashashmap.hxx>
 
+#include <com/sun/star/connection/XConnection.hpp>
 #include <com/sun/star/document/MacroExecMode.hpp>
 #include <com/sun/star/beans/XPropertySet.hpp>
 #include <com/sun/star/container/XNameAccess.hpp>
@@ -108,6 +110,10 @@
 #include <com/sun/star/text/TextContentAnchorType.hpp>
 #include <com/sun/star/document/XRedlinesSupplier.hpp>
 #include <com/sun/star/ui/GlobalAcceleratorConfiguration.hpp>
+#include <com/sun/star/bridge/BridgeFactory.hpp>
+#include <com/sun/star/bridge/XBridgeFactory.hpp>
+#include <com/sun/star/bridge/XBridge.hpp>
+#include <com/sun/star/uno/XNamingService.hpp>
 
 #include <com/sun/star/xml/crypto/SEInitializer.hpp>
 #include <com/sun/star/xml/crypto/XSEInitializer.hpp>
@@ -121,6 +127,7 @@
 #include <com/sun/star/i18n/LocaleCalendar2.hpp>
 #include <com/sun/star/i18n/ScriptType.hpp>
 #include <com/sun/star/lang/DisposedException.hpp>
+#include <com/sun/star/lang/XMultiServiceFactory.hpp>
 
 #include <editeng/flstitem.hxx>
 #ifdef IOS
@@ -228,6 +235,9 @@ using namespace css;
 using namespace vcl;
 using namespace desktop;
 using namespace utl;
+using namespace bridge;
+using namespace uno;
+using namespace lang;
 
 static LibLibreOffice_Impl *gImpl = nullptr;
 static bool lok_preinit_2_called = false;
@@ -2567,6 +2577,13 @@ static char* lo_extractRequest(LibreOfficeKit* pThis,
 
 static void lo_trimMemory(LibreOfficeKit* pThis, int nTarget);
 
+static int
+lo_startURP(LibreOfficeKit* pThis, void* pReceiveURPFromLOContext, void** 
pSendURPToLOContext,
+            int (*fnReceiveURPFromLO)(void* pContext, const signed char* 
pBuffer, int nLen),
+            int (**pfnSendURPToLO)(void* pContext, const signed char* pBuffer, 
int nLen));
+
+static void lo_stopURP(LibreOfficeKit* pThis, void* pSendURPToLOContext);
+
 static void lo_runLoop(LibreOfficeKit* pThis,
                        LibreOfficeKitPollCallback pPollCallback,
                        LibreOfficeKitWakeCallback pWakeCallback,
@@ -2609,6 +2626,8 @@ LibLibreOffice_Impl::LibLibreOffice_Impl()
         m_pOfficeClass->dumpState = lo_dumpState;
         m_pOfficeClass->extractRequest = lo_extractRequest;
         m_pOfficeClass->trimMemory = lo_trimMemory;
+        m_pOfficeClass->startURP = lo_startURP;
+        m_pOfficeClass->stopURP = lo_stopURP;
 
         gOfficeClass = m_pOfficeClass;
     }
@@ -3160,6 +3179,214 @@ static void lo_trimMemory(LibreOfficeKit* /* pThis */, 
int nTarget)
     }
 }
 
+namespace
+{
+class FunctionBasedURPInstanceProvider
+    : public ::cppu::WeakImplHelper<css::bridge::XInstanceProvider>
+{
+private:
+    css::uno::Reference<css::uno::XComponentContext> m_rContext;
+
+public:
+    FunctionBasedURPInstanceProvider(
+        const css::uno::Reference<css::uno::XComponentContext>& rxContext);
+
+    // XInstanceProvider
+    virtual css::uno::Reference<css::uno::XInterface>
+        SAL_CALL getInstance(const OUString& aName) override;
+};
+
+// InstanceProvider
+FunctionBasedURPInstanceProvider::FunctionBasedURPInstanceProvider(
+    const Reference<XComponentContext>& rxContext)
+    : m_rContext(rxContext)
+{
+}
+
+Reference<XInterface> FunctionBasedURPInstanceProvider::getInstance(const 
OUString& aName)
+{
+    Reference<XInterface> rInstance;
+
+    if (aName == "StarOffice.ServiceManager")
+    {
+        rInstance.set(m_rContext->getServiceManager());
+    }
+    else if (aName == "StarOffice.ComponentContext")
+    {
+        rInstance = m_rContext;
+    }
+    else if (aName == "StarOffice.NamingService")
+    {
+        Reference<XNamingService> rNamingService(
+            m_rContext->getServiceManager()->createInstanceWithContext(
+                "com.sun.star.uno.NamingService", m_rContext),
+            UNO_QUERY);
+        if (rNamingService.is())
+        {
+            rNamingService->registerObject("StarOffice.ServiceManager",
+                                           m_rContext->getServiceManager());
+            rNamingService->registerObject("StarOffice.ComponentContext", 
m_rContext);
+            rInstance = rNamingService;
+        }
+    }
+    return rInstance;
+}
+
+class FunctionBasedURPConnection : public 
cppu::WeakImplHelper<css::connection::XConnection>
+{
+public:
+    explicit FunctionBasedURPConnection(void*, int (*)(void* pContext, const 
signed char* pBuffer,
+                                                       int nLen));
+    ~FunctionBasedURPConnection();
+
+    // These overridden member functions use "read" and "write" from the point 
of view of LO,
+    // i.e. the opposite to how startURP() uses them.
+    virtual sal_Int32 SAL_CALL read(Sequence<sal_Int8>& aReadBytes,
+                                    sal_Int32 nBytesToRead) override;
+    virtual void SAL_CALL write(const Sequence<sal_Int8>& aData) override;
+    virtual void SAL_CALL flush() override;
+    virtual void SAL_CALL close() override;
+    virtual OUString SAL_CALL getDescription() override;
+    void setBridge(Reference<XBridge>);
+    int addClientURPToBuffer(const signed char* pBuffer, int nLen);
+    void* getContext();
+    inline static int g_connectionCount = 0;
+
+private:
+    std::shared_ptr<std::deque<signed char>> m_pBuffer;
+    void* m_pRecieveFromLOContext;
+    int (*m_fnReceiveURPFromLO)(void* pContext, const signed char* pBuffer, 
int nLen);
+    Reference<XBridge> m_URPBridge;
+    std::atomic<bool> m_closed = false;
+    std::condition_variable m_URPInBuffer;
+    std::mutex m_bufferMutex;
+};
+
+FunctionBasedURPConnection::FunctionBasedURPConnection(
+    void* pRecieveFromLOContext,
+    int (*fnRecieveFromLO)(void* pContext, const signed char* pBuffer, int 
nLen))
+    : m_pBuffer(std::make_shared<std::deque<signed char>>())
+    , m_pRecieveFromLOContext(pRecieveFromLOContext)
+    , m_fnReceiveURPFromLO(fnRecieveFromLO)
+{
+    g_connectionCount++;
+}
+
+FunctionBasedURPConnection::~FunctionBasedURPConnection()
+{
+    Reference<XComponent> xComp(m_URPBridge, UNO_QUERY_THROW);
+    xComp->dispose(); // TODO: check this doesn't deadlock
+}
+
+int sendURPToLO(void* pContext /* FunctionBasedURPConnection* */, const signed 
char* pBuffer,
+                int nLen)
+{
+    return 
static_cast<FunctionBasedURPConnection*>(pContext)->addClientURPToBuffer(pBuffer,
 nLen);
+}
+
+int FunctionBasedURPConnection::addClientURPToBuffer(const signed char* 
pBuffer, int nLen)
+{
+    {
+        std::scoped_lock lock(m_bufferMutex);
+
+        if (m_closed)
+        {
+            // We can't write URP to a closed connection
+            SAL_WARN("lok.urp", "A client attempted to write URP to a closed "
+                            "FunctionBasedURPConnection... ignoring");
+            return 0;
+        }
+        m_pBuffer->insert(m_pBuffer->end(), pBuffer, pBuffer + nLen);
+    }
+    m_URPInBuffer.notify_one();
+    return nLen;
+}
+
+void* FunctionBasedURPConnection::getContext() { return this; }
+
+sal_Int32 FunctionBasedURPConnection::read(Sequence<sal_Int8>& aReadBytes, 
sal_Int32 nBytesToRead)
+{
+    if (aReadBytes.getLength() != nBytesToRead)
+    {
+        aReadBytes.realloc(nBytesToRead);
+    }
+
+    sal_Int8* result = aReadBytes.getArray();
+    // As with osl::StreamPipe, we must always read nBytesToRead...
+
+    {
+        std::unique_lock lock(m_bufferMutex);
+
+        if (nBytesToRead < 0)
+        {
+            return 0;
+        }
+        m_URPInBuffer.wait(
+            lock, [this, nBytesToRead] { return 
static_cast<sal_Int32>(m_pBuffer->size()) >= nBytesToRead; });
+
+        std::copy(m_pBuffer->begin(), m_pBuffer->begin() + nBytesToRead, 
result);
+        m_pBuffer->erase(m_pBuffer->begin(), m_pBuffer->begin() + 
nBytesToRead);
+    }
+
+    return nBytesToRead;
+}
+
+void FunctionBasedURPConnection::write(const Sequence<sal_Int8>& aData)
+{
+    m_fnReceiveURPFromLO(m_pRecieveFromLOContext, aData.getConstArray(), 
aData.getLength());
+}
+
+void FunctionBasedURPConnection::flush() {}
+
+void FunctionBasedURPConnection::close()
+{
+    SAL_INFO("lok.urp", "Requested to close FunctionBasedURPConnection");
+    m_closed = true;
+}
+
+OUString FunctionBasedURPConnection::getDescription() { return ""; }
+
+void FunctionBasedURPConnection::setBridge(Reference<XBridge> xBridge) { 
m_URPBridge = xBridge; }
+}
+
+static int
+lo_startURP(LibreOfficeKit* /* pThis */, void* pRecieveFromLOContext, void** 
ppSendToLOContext,
+            int (*fnReceiveURPFromLO)(void* pContext, const signed char* 
pBuffer, int nLen),
+            int (**pfnSendURPToLO)(void* pContext, const signed char* pBuffer, 
int nLen))
+{
+    // Here we will roughly do what desktop LO does when one passes a 
command-line switch like
+    // --accept=socket,port=nnnn;urp;StarOffice.ServiceManager. Except that no 
listening socket will
+    // be created. The communication to the URP will be through the 
fnReceiveURPFromLO and pfnSendURPToLO functions.
+
+    rtl::Reference<FunctionBasedURPConnection> connection(
+        new FunctionBasedURPConnection(pRecieveFromLOContext, 
fnReceiveURPFromLO));
+
+    *pfnSendURPToLO = sendURPToLO;
+    *ppSendToLOContext = connection->getContext();
+
+    Reference<XBridgeFactory> xBridgeFactory = 
css::bridge::BridgeFactory::create(xContext);
+
+    Reference<XInstanceProvider> xInstanceProvider(new 
FunctionBasedURPInstanceProvider(xContext));
+
+    Reference<XBridge> xBridge(xBridgeFactory->createBridge(
+        "functionurp" + 
OUString::number(FunctionBasedURPConnection::g_connectionCount), "urp",
+        connection, xInstanceProvider));
+
+    connection->setBridge(std::move(xBridge));
+
+    return true;
+}
+
+/**
+ * Stop a function based URP connection that you started with lo_startURP above
+ *
+ * @param pSendToLOContext a pointer to the context you got back using your 
ppSendToLOContext before */
+static void lo_stopURP(LibreOfficeKit* /* pThis */,
+                       void* pSendToLOContext /* FunctionBasedURPConnection* 
*/)
+{
+    static_cast<FunctionBasedURPConnection*>(pSendToLOContext)->close();
+}
+
 static void lo_registerCallback (LibreOfficeKit* pThis,
                                  LibreOfficeKitCallback pCallback,
                                  void* pData)
diff --git a/download.lst b/download.lst
index 02adb7a75fd4..212077f775b6 100644
--- a/download.lst
+++ b/download.lst
@@ -249,6 +249,11 @@ ICU_DATA_TARBALL := icu4c-73_2-data.zip
 # three static lines
 # so that git cherry-pick
 # will not run into conflicts
+JAVA_WEBSOCKET_SHA256SUM := 
a6828b35d1f938fee2335945f3d3c563cbbfa58ce7eb0bf72778d0fa7a550720
+JAVA_WEBSOCKET_TARBALL := Java-WebSocket-1.5.4.tar.gz
+# three static lines
+# so that git cherry-pick
+# will not run into conflicts
 JFREEREPORT_FLOW_ENGINE_SHA256SUM := 
233f66e8d25c5dd971716d4200203a612a407649686ef3b52075d04b4c9df0dd
 JFREEREPORT_FLOW_ENGINE_TARBALL := 
ba2930200c9f019c2d93a8c88c651a0f-flow-engine-0.9.4.zip
 JFREEREPORT_FLUTE_SHA256SUM := 
1b5b24f7bc543c0362b667692f78db8bab4ed6dafc6172f104d0bd3757d8a133
diff --git a/external/Module_external.mk b/external/Module_external.mk
index 9feb4e21addf..ad719e7c4c37 100644
--- a/external/Module_external.mk
+++ b/external/Module_external.mk
@@ -50,6 +50,7 @@ $(eval $(call gb_Module_add_moduledirs,external,\
        $(call gb_Helper_optional,HUNSPELL,hunspell) \
        $(call gb_Helper_optional,HYPHEN,hyphen) \
        $(call gb_Helper_optional,ICU,icu) \
+       $(call gb_Helper_optional,JAVA_WEBSOCKET,java_websocket) \
        $(call gb_Helper_optional,JFREEREPORT,jfreereport) \
        $(call gb_Helper_optional,LIBJPEG_TURBO,libjpeg-turbo) \
        $(call gb_Helper_optional,LCMS2,lcms2) \
diff --git a/external/java_websocket/ExternalPackage_java_websocket.mk 
b/external/java_websocket/ExternalPackage_java_websocket.mk
new file mode 100644
index 000000000000..2b3d1d87f3c0
--- /dev/null
+++ b/external/java_websocket/ExternalPackage_java_websocket.mk
@@ -0,0 +1,16 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+$(eval $(call 
gb_ExternalPackage_ExternalPackage,java_websocket_project,java_websocket))
+
+$(eval $(call 
gb_ExternalPackage_use_external_project,java_websocket_project,java_websocket))
+
+$(eval $(call 
gb_ExternalPackage_add_file,java_websocket_project,$(LIBO_SHARE_JAVA_FOLDER)/java_websocket.jar,dist/java_websocket.jar))
+
+# vim: set noet sw=4 ts=4:
diff --git a/external/java_websocket/ExternalProject_java_websocket.mk 
b/external/java_websocket/ExternalProject_java_websocket.mk
new file mode 100644
index 000000000000..0d713aadc02d
--- /dev/null
+++ b/external/java_websocket/ExternalProject_java_websocket.mk
@@ -0,0 +1,31 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+$(eval $(call gb_ExternalProject_ExternalProject,java_websocket))
+
+$(eval $(call gb_ExternalProject_register_targets,java_websocket,\
+       build \
+))
+
+$(call gb_ExternalProject_get_state_target,java_websocket,build) :
+       $(call gb_Trace_StartRange,java_websocket,EXTERNAL)
+       $(call gb_ExternalProject_run,build,\
+               JAVA_HOME=$(JAVA_HOME_FOR_BUILD) \
+               $(ICECREAM_RUN) "$(ANT)" \
+                       $(if $(verbose),-v,-q) \
+                       -f build.xml \
+                       
-Dbuild.label="build-$(LIBO_VERSION_MAJOR).$(LIBO_VERSION_MINOR).$(LIBO_VERSION_MICRO).$(LIBO_VERSION_PATCH)"
 \
+                       -Dant.build.javac.source=$(JAVA_SOURCE_VER) \
+                       -Dant.build.javac.target=$(JAVA_TARGET_VER) \
+                       $(if $(debug),-Dbuild.debug="on") \
+                       jar \
+       )
+       $(call gb_Trace_EndRange,java_websocket,EXTERNAL)
+
+# vim: set noet sw=4 ts=4:
diff --git a/external/java_websocket/Makefile b/external/java_websocket/Makefile
new file mode 100644
index 000000000000..e4968cf85fb6
--- /dev/null
+++ b/external/java_websocket/Makefile
@@ -0,0 +1,7 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+
+module_directory:=$(dir $(realpath $(firstword $(MAKEFILE_LIST))))
+
+include $(module_directory)/../../solenv/gbuild/partial_build.mk
+
+# vim: set noet sw=4 ts=4:
diff --git a/external/java_websocket/Module_java_websocket.mk 
b/external/java_websocket/Module_java_websocket.mk
new file mode 100644
index 000000000000..3dcb12dde43e
--- /dev/null
+++ b/external/java_websocket/Module_java_websocket.mk
@@ -0,0 +1,18 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+$(eval $(call gb_Module_Module,java_websocket))
+
+$(eval $(call gb_Module_add_targets,java_websocket,\
+       ExternalPackage_java_websocket \
+       ExternalProject_java_websocket \
+       UnpackedTarball_java_websocket \
+))
+
+# vim: set noet sw=4 ts=4:
diff --git a/external/java_websocket/README b/external/java_websocket/README
new file mode 100644
index 000000000000..def0298a09cb
--- /dev/null
+++ b/external/java_websocket/README
@@ -0,0 +1,3 @@
+Java WebSocket Implmentation from 
[https://github.com/TooTallNate/Java-WebSocket].
+
+To send data over websockets, enabling uno over websockets.
diff --git a/external/java_websocket/UnpackedTarball_java_websocket.mk 
b/external/java_websocket/UnpackedTarball_java_websocket.mk
new file mode 100644
index 000000000000..cc8e7c259fc9
--- /dev/null
+++ b/external/java_websocket/UnpackedTarball_java_websocket.mk
@@ -0,0 +1,21 @@
+# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+$(eval $(call gb_UnpackedTarball_UnpackedTarball,java_websocket))
+
+$(eval $(call 
gb_UnpackedTarball_set_tarball,java_websocket,$(JAVA_WEBSOCKET_TARBALL),,java_websocket))
+
+$(eval $(call gb_UnpackedTarball_set_patchlevel,java_websocket,1))
+
+$(eval $(call gb_UnpackedTarball_add_patches,java_websocket,\
+       external/java_websocket/patches/ant-build.patch \
+       external/java_websocket/patches/no-slf4j.patch \
+))
+
+# vim: set noet sw=4 ts=4:
diff --git a/external/java_websocket/patches/ant-build.patch 
b/external/java_websocket/patches/ant-build.patch
new file mode 100644
index 000000000000..1757417d5122
--- /dev/null
+++ b/external/java_websocket/patches/ant-build.patch
@@ -0,0 +1,26 @@
+--- /dev/null  1970-01-01 01:00:00.000000000 +0100
++++ b/build.xml        2023-08-30 11:43:05.152647141 +0100
+@@ -0,0 +1,23 @@
++<project default="all">
++    <target name="all" depends="jar" />
++
++    <target name="compile">
++        <mkdir dir="build/classes" />
++        <javac includeantruntime="false" srcdir="src/main/java"
++               destdir="build/classes">
++        </javac>
++    </target>
++
++    <target name="jar" depends="compile">
++        <mkdir dir="dist"/>
++        <jar destfile="dist/java_websocket.jar">
++            <fileset dir="build/classes" includes="**/*.class" />
++        </jar>
++    </target>
++
++    <target name="clean">
++        <delete dir="build" />
++        <delete dir="dist" />
++    </target>
++
++</project>
diff --git a/external/java_websocket/patches/no-slf4j.patch 
b/external/java_websocket/patches/no-slf4j.patch
new file mode 100644
index 000000000000..27296071eff7
--- /dev/null
+++ b/external/java_websocket/patches/no-slf4j.patch
@@ -0,0 +1,748 @@
+diff -ru a/src/main/java/org/java_websocket/AbstractWebSocket.java 
b/src/main/java/org/java_websocket/AbstractWebSocket.java
+--- a/src/main/java/org/java_websocket/AbstractWebSocket.java  2023-07-20 
21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/AbstractWebSocket.java  2023-08-30 
12:06:11.004719499 +0100
+@@ -33,9 +33,7 @@
+ import java.util.concurrent.TimeUnit;
+ import org.java_websocket.framing.CloseFrame;
+ import org.java_websocket.util.NamedThreadFactory;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
+-
++import java.util.logging.Logger;
+ 
+ /**
+  * Base class for additional implementations for the server as well as the 
client
+@@ -47,7 +45,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(AbstractWebSocket.class);
++  private final Logger log = 
Logger.getLogger(AbstractWebSocket.class.getName());
+ 
+   /**
+    * Attribute which allows you to deactivate the Nagle's algorithm
+@@ -118,12 +116,12 @@
+     synchronized (syncConnectionLost) {
+       this.connectionLostTimeout = 
TimeUnit.SECONDS.toNanos(connectionLostTimeout);
+       if (this.connectionLostTimeout <= 0) {
+-        log.trace("Connection lost timer stopped");
++        log.fine("Connection lost timer stopped");
+         cancelConnectionLostTimer();
+         return;
+       }
+       if (this.websocketRunning) {
+-        log.trace("Connection lost timer restarted");
++        log.fine("Connection lost timer restarted");
+         //Reset all the pings
+         try {
+           ArrayList<WebSocket> connections = new 
ArrayList<>(getConnections());
+@@ -135,7 +133,7 @@
+             }
+           }
+         } catch (Exception e) {
+-          log.error("Exception during connection lost restart", e);
++          log.severe("Exception during connection lost restart" + " : " + e);
+         }
+         restartConnectionLostTimer();
+       }
+@@ -151,7 +149,7 @@
+     synchronized (syncConnectionLost) {
+       if (connectionLostCheckerService != null || connectionLostCheckerFuture 
!= null) {
+         this.websocketRunning = false;
+-        log.trace("Connection lost timer stopped");
++        log.fine("Connection lost timer stopped");
+         cancelConnectionLostTimer();
+       }
+     }
+@@ -165,10 +163,10 @@
+   protected void startConnectionLostTimer() {
+     synchronized (syncConnectionLost) {
+       if (this.connectionLostTimeout <= 0) {
+-        log.trace("Connection lost timer deactivated");
++        log.fine("Connection lost timer deactivated");
+         return;
+       }
+-      log.trace("Connection lost timer started");
++      log.fine("Connection lost timer started");
+       this.websocketRunning = true;
+       restartConnectionLostTimer();
+     }
+@@ -228,14 +226,14 @@
+     }
+     WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket;
+     if (webSocketImpl.getLastPong() < minimumPongTime) {
+-      log.trace("Closing connection due to no pong received: {}", 
webSocketImpl);
++      log.fine("Closing connection due to no pong received: {}" + " : " + 
webSocketImpl);
+       webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE,
+           "The connection was closed because the other endpoint did not 
respond with a pong in time. For more information check: 
https://github.com/TooTallNate/Java-WebSocket/wiki/Lost-connection-detection";);
+     } else {
+       if (webSocketImpl.isOpen()) {
+         webSocketImpl.sendPing();
+       } else {
+-        log.trace("Trying to ping a non open connection: {}", webSocketImpl);
++        log.fine("Trying to ping a non open connection: {}" + " : " + 
webSocketImpl);
+       }
+     }
+   }
+diff -ru a/src/main/java/org/java_websocket/drafts/Draft_6455.java 
b/src/main/java/org/java_websocket/drafts/Draft_6455.java
+--- a/src/main/java/org/java_websocket/drafts/Draft_6455.java  2023-07-20 
21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/drafts/Draft_6455.java  2023-08-30 
12:16:03.534083539 +0100
+@@ -66,8 +66,8 @@
+ import org.java_websocket.protocols.Protocol;
+ import org.java_websocket.util.Base64;
+ import org.java_websocket.util.Charsetfunctions;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
++import java.util.logging.Level;
++import java.util.logging.Logger;
+ 
+ /**
+  * Implementation for the RFC 6455 websocket protocol This is the recommended 
class for your
+@@ -110,7 +110,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(Draft_6455.class);
++  private final Logger log = Logger.getLogger(Draft_6455.class.getName());
+ 
+   /**
+    * Attribute for the used extension in this draft
+@@ -263,7 +263,7 @@
+       throws InvalidHandshakeException {
+     int v = readVersion(handshakedata);
+     if (v != 13) {
+-      log.trace("acceptHandshakeAsServer - Wrong websocket version.");
++      log.fine("acceptHandshakeAsServer - Wrong websocket version.");
+       return HandshakeState.NOT_MATCHED;
+     }
+     HandshakeState extensionState = HandshakeState.NOT_MATCHED;
+@@ -272,7 +272,7 @@
+       if (knownExtension.acceptProvidedExtensionAsServer(requestedExtension)) 
{
+         negotiatedExtension = knownExtension;
+         extensionState = HandshakeState.MATCHED;
+-        log.trace("acceptHandshakeAsServer - Matching extension found: {}", 
negotiatedExtension);
++        log.fine("acceptHandshakeAsServer - Matching extension found: {}" + " 
: " + negotiatedExtension);
+         break;
+       }
+     }
+@@ -281,7 +281,7 @@
+     if (protocolState == HandshakeState.MATCHED && extensionState == 
HandshakeState.MATCHED) {
+       return HandshakeState.MATCHED;
+     }
+-    log.trace("acceptHandshakeAsServer - No matching extension or protocol 
found.");
++    log.fine("acceptHandshakeAsServer - No matching extension or protocol 
found.");
+     return HandshakeState.NOT_MATCHED;
+   }
+ 
+@@ -295,7 +295,7 @@
+     for (IProtocol knownProtocol : knownProtocols) {
+       if (knownProtocol.acceptProvidedProtocol(requestedProtocol)) {
+         protocol = knownProtocol;
+-        log.trace("acceptHandshake - Matching protocol found: {}", protocol);
++        log.fine("acceptHandshake - Matching protocol found: {}" + " : " + 
protocol);
+         return HandshakeState.MATCHED;
+       }
+     }
+@@ -306,12 +306,12 @@
+   public HandshakeState acceptHandshakeAsClient(ClientHandshake request, 
ServerHandshake response)
+       throws InvalidHandshakeException {
+     if (!basicAccept(response)) {
+-      log.trace("acceptHandshakeAsClient - Missing/wrong upgrade or 
connection in handshake.");
++      log.fine("acceptHandshakeAsClient - Missing/wrong upgrade or connection 
in handshake.");
+       return HandshakeState.NOT_MATCHED;
+     }
+     if (!request.hasFieldValue(SEC_WEB_SOCKET_KEY) || !response
+         .hasFieldValue(SEC_WEB_SOCKET_ACCEPT)) {
+-      log.trace("acceptHandshakeAsClient - Missing Sec-WebSocket-Key or 
Sec-WebSocket-Accept");
++      log.fine("acceptHandshakeAsClient - Missing Sec-WebSocket-Key or 
Sec-WebSocket-Accept");
+       return HandshakeState.NOT_MATCHED;
+     }
+ 
+@@ -320,7 +320,7 @@
+     seckeyChallenge = generateFinalKey(seckeyChallenge);
+ 
+     if (!seckeyChallenge.equals(seckeyAnswer)) {
+-      log.trace("acceptHandshakeAsClient - Wrong key for Sec-WebSocket-Key.");
++      log.fine("acceptHandshakeAsClient - Wrong key for Sec-WebSocket-Key.");
+       return HandshakeState.NOT_MATCHED;
+     }
+     HandshakeState extensionState = HandshakeState.NOT_MATCHED;
+@@ -329,7 +329,7 @@
+       if (knownExtension.acceptProvidedExtensionAsClient(requestedExtension)) 
{
+         negotiatedExtension = knownExtension;
+         extensionState = HandshakeState.MATCHED;
+-        log.trace("acceptHandshakeAsClient - Matching extension found: {}", 
negotiatedExtension);
++        log.fine("acceptHandshakeAsClient - Matching extension found: {}" + " 
: " + negotiatedExtension);
+         break;
+       }
+     }
+@@ -338,7 +338,7 @@
+     if (protocolState == HandshakeState.MATCHED && extensionState == 
HandshakeState.MATCHED) {
+       return HandshakeState.MATCHED;
+     }
+-    log.trace("acceptHandshakeAsClient - No matching extension or protocol 
found.");
++    log.fine("acceptHandshakeAsClient - No matching extension or protocol 
found.");
+     return HandshakeState.NOT_MATCHED;
+   }
+ 
+@@ -467,8 +467,8 @@
+   @Override
+   public ByteBuffer createBinaryFrame(Framedata framedata) {
+     getExtension().encodeFrame(framedata);
+-    if (log.isTraceEnabled()) {
+-      log.trace("afterEnconding({}): {}", 
framedata.getPayloadData().remaining(),
++    if (log.isLoggable(Level.FINE)) {
++      log.fine("afterEnconding({}): {}" + " : " + 
framedata.getPayloadData().remaining() + " : " +
+           (framedata.getPayloadData().remaining() > 1000 ? "too big to 
display"
+               : new String(framedata.getPayloadData().array())));
+     }
+@@ -587,8 +587,8 @@
+     }
+     currentDecodingExtension.isFrameValid(frame);
+     currentDecodingExtension.decodeFrame(frame);
+-    if (log.isTraceEnabled()) {
+-      log.trace("afterDecoding({}): {}", frame.getPayloadData().remaining(),
++    if (log.isLoggable(Level.FINE)) {
++      log.fine("afterDecoding({}): {}" + " : " + 
frame.getPayloadData().remaining() + " : " +
+           (frame.getPayloadData().remaining() > 1000 ? "too big to display"
+               : new String(frame.getPayloadData().array())));
+     }
+@@ -615,7 +615,7 @@
+     int payloadlength = oldPayloadlength;
+     int realpacketsize = oldRealpacketsize;
+     if (optcode == Opcode.PING || optcode == Opcode.PONG || optcode == 
Opcode.CLOSING) {
+-      log.trace("Invalid frame: more than 125 octets");
++      log.fine("Invalid frame: more than 125 octets");
+       throw new InvalidFrameException("more than 125 octets");
+     }
+     if (payloadlength == 126) {
+@@ -647,15 +647,15 @@
+    */
+   private void translateSingleFrameCheckLengthLimit(long length) throws 
LimitExceededException {
+     if (length > Integer.MAX_VALUE) {
+-      log.trace("Limit exedeed: Payloadsize is to big...");
++      log.fine("Limit exedeed: Payloadsize is to big...");
+       throw new LimitExceededException("Payloadsize is to big...");
+     }
+     if (length > maxFrameSize) {
+-      log.trace("Payload limit reached. Allowed: {} Current: {}", 
maxFrameSize, length);
++      log.fine("Payload limit reached. Allowed: {} Current: {}" + " : " + 
maxFrameSize + " : " + length);
+       throw new LimitExceededException("Payload limit reached.", 
maxFrameSize);
+     }
+     if (length < 0) {
+-      log.trace("Limit underflow: Payloadsize is to little...");
++      log.fine("Limit underflow: Payloadsize is to little...");
+       throw new LimitExceededException("Payloadsize is to little...");
+     }
+   }
+@@ -670,7 +670,7 @@
+   private void translateSingleFrameCheckPacketSize(int maxpacketsize, int 
realpacketsize)
+       throws IncompleteException {
+     if (maxpacketsize < realpacketsize) {
+-      log.trace("Incomplete frame: maxpacketsize < realpacketsize");
++      log.fine("Incomplete frame: maxpacketsize < realpacketsize");
+       throw new IncompleteException(realpacketsize);
+     }
+   }
+@@ -903,7 +903,7 @@
+     } else if (!frame.isFin() || curop == Opcode.CONTINUOUS) {
+       processFrameContinuousAndNonFin(webSocketImpl, frame, curop);
+     } else if (currentContinuousFrame != null) {
+-      log.error("Protocol error: Continuous frame sequence not completed.");
++      log.severe("Protocol error: Continuous frame sequence not completed.");
+       throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+           "Continuous frame sequence not completed.");
+     } else if (curop == Opcode.TEXT) {
+@@ -911,7 +911,7 @@
+     } else if (curop == Opcode.BINARY) {
+       processFrameBinary(webSocketImpl, frame);
+     } else {
+-      log.error("non control or continious frame expected");
++      log.severe("non control or continious frame expected");
+       throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+           "non control or continious frame expected");
+     }
+@@ -932,13 +932,13 @@
+     } else if (frame.isFin()) {
+       processFrameIsFin(webSocketImpl, frame);
+     } else if (currentContinuousFrame == null) {
+-      log.error("Protocol error: Continuous frame sequence was not started.");
++      log.severe("Protocol error: Continuous frame sequence was not 
started.");
+       throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+           "Continuous frame sequence was not started.");
+     }
+     //Check if the whole payload is valid utf8, when the opcode indicates a 
text
+     if (curop == Opcode.TEXT && 
!Charsetfunctions.isValidUTF8(frame.getPayloadData())) {
+-      log.error("Protocol error: Payload is not UTF8");
++      log.severe("Protocol error: Payload is not UTF8");
+       throw new InvalidDataException(CloseFrame.NO_UTF8);
+     }
+     //Checking if the current continuous frame contains a correct payload 
with the other frames combined
+@@ -969,7 +969,7 @@
+    * @param e             the runtime exception
+    */
+   private void logRuntimeException(WebSocketImpl webSocketImpl, 
RuntimeException e) {
+-    log.error("Runtime exception during onWebsocketMessage", e);
++    log.severe("Runtime exception during onWebsocketMessage" + " : " + e);
+     webSocketImpl.getWebSocketListener().onWebsocketError(webSocketImpl, e);
+   }
+ 
+@@ -999,7 +999,7 @@
+   private void processFrameIsFin(WebSocketImpl webSocketImpl, Framedata frame)
+       throws InvalidDataException {
+     if (currentContinuousFrame == null) {
+-      log.trace("Protocol error: Previous continuous frame sequence not 
completed.");
++      log.fine("Protocol error: Previous continuous frame sequence not 
completed.");
+       throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+           "Continuous frame sequence was not started.");
+     }
+@@ -1036,7 +1036,7 @@
+    */
+   private void processFrameIsNotFin(Framedata frame) throws 
InvalidDataException {
+     if (currentContinuousFrame != null) {
+-      log.trace("Protocol error: Previous continuous frame sequence not 
completed.");
++      log.fine("Protocol error: Previous continuous frame sequence not 
completed.");
+       throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR,
+           "Previous continuous frame sequence not completed.");
+     }
+@@ -1102,7 +1102,7 @@
+     long totalSize = getByteBufferListSize();
+     if (totalSize > maxFrameSize) {
+       clearBufferList();
+-      log.trace("Payload limit reached. Allowed: {} Current: {}", 
maxFrameSize, totalSize);
++      log.fine("Payload limit reached. Allowed: {} Current: {}" + " : " + 
maxFrameSize + " : " + totalSize);
+       throw new LimitExceededException(maxFrameSize);
+     }
+   }
+diff -ru a/src/main/java/org/java_websocket/server/WebSocketServer.java 
b/src/main/java/org/java_websocket/server/WebSocketServer.java
+--- a/src/main/java/org/java_websocket/server/WebSocketServer.java     
2023-07-20 21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/server/WebSocketServer.java     
2023-08-30 12:06:46.372798355 +0100
+@@ -67,8 +67,7 @@
+ import org.java_websocket.framing.Framedata;
+ import org.java_websocket.handshake.ClientHandshake;
+ import org.java_websocket.handshake.Handshakedata;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
++import java.util.logging.Logger;
+ 
+ /**
+  * <tt>WebSocketServer</tt> is an abstract class that only takes care of the
+@@ -84,7 +83,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
++  private final Logger log = 
Logger.getLogger(WebSocketServer.class.getName());
+ 
+   /**
+    * Holds the list of active WebSocket connections. "Active" means WebSocket 
handshake is complete
+@@ -611,7 +610,7 @@
+       try {
+         selector.close();
+       } catch (IOException e) {
+-        log.error("IOException during selector.close", e);
++        log.severe("IOException during selector.close" + " : " + e);
+         onError(null, e);
+       }
+     }
+@@ -619,7 +618,7 @@
+       try {
+         server.close();
+       } catch (IOException e) {
+-        log.error("IOException during server.close", e);
++        log.severe("IOException during server.close" + " : " + e);
+         onError(null, e);
+       }
+     }
+@@ -677,13 +676,13 @@
+         } catch (IOException e) {
+           // there is nothing that must be done here
+         }
+-        log.trace("Connection closed because of exception", ex);
++        log.fine("Connection closed because of exception" + " : " + ex);
+       }
+     }
+   }
+ 
+   private void handleFatal(WebSocket conn, Exception e) {
+-    log.error("Shutdown due to fatal error", e);
++    log.severe("Shutdown due to fatal error" + " : " + e);
+     onError(conn, e);
+ 
+     String causeMessage = e.getCause() != null ? " caused by " + 
e.getCause().getClass().getName() : "";
+@@ -692,7 +691,7 @@
+       stop(0, errorMessage);
+     } catch (InterruptedException e1) {
+       Thread.currentThread().interrupt();
+-      log.error("Interrupt during stop", e);
++      log.severe("Interrupt during stop" + " : " + e);
+       onError(null, e1);
+     }
+ 
+@@ -760,8 +759,8 @@
+         removed = this.connections.remove(ws);
+       } else {
+         //Don't throw an assert error if the ws is not in the list. e.g. when 
the other endpoint did not send any handshake. see #512
+-        log.trace(
+-            "Removing connection which is not in the connections collection! 
Possible no handshake received! {}",
++        log.fine(
++            "Removing connection which is not in the connections collection! 
Possible no handshake received! {}" + " : " +
+             ws);
+       }
+     }
+@@ -1065,7 +1064,7 @@
+       setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+         @Override
+         public void uncaughtException(Thread t, Throwable e) {
+-          log.error("Uncaught exception in thread {}: {}", t.getName(), e);
++          log.severe("Uncaught exception in thread {}: {}" + " : " + 
t.getName() + " : " + e);
+         }
+       });
+     }
+@@ -1089,11 +1088,11 @@
+       } catch (InterruptedException e) {
+         Thread.currentThread().interrupt();
+       } catch (VirtualMachineError | ThreadDeath | LinkageError e) {
+-        log.error("Got fatal error in worker thread {}", getName());
++        log.severe("Got fatal error in worker thread {}" + " : " + getName());
+         Exception exception = new Exception(e);
+         handleFatal(ws, exception);
+       } catch (Throwable e) {
+-        log.error("Uncaught exception in thread {}: {}", getName(), e);
++        log.severe("Uncaught exception in thread {}: {}" + " : " + getName() 
+ " : " + e);
+         if (ws != null) {
+           Exception exception = new Exception(e);
+           onWebsocketError(ws, exception);
+@@ -1113,7 +1112,7 @@
+       try {
+         ws.decode(buf);
+       } catch (Exception e) {
+-        log.error("Error while reading from remote connection", e);
++        log.severe("Error while reading from remote connection" + " : " + e);
+       } finally {
+         pushBuffer(buf);
+       }
+diff -ru a/src/main/java/org/java_websocket/SSLSocketChannel2.java 
b/src/main/java/org/java_websocket/SSLSocketChannel2.java
+--- a/src/main/java/org/java_websocket/SSLSocketChannel2.java  2023-07-20 
21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/SSLSocketChannel2.java  2023-08-30 
12:05:33.937636854 +0100
+@@ -47,8 +47,8 @@
+ import javax.net.ssl.SSLException;
+ import javax.net.ssl.SSLSession;
+ import org.java_websocket.interfaces.ISSLChannel;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
++import java.util.logging.Level;
++import java.util.logging.Logger;
+ 
+ /**
+  * Implements the relevant portions of the SocketChannel interface with the 
SSLEngine wrapper.
+@@ -66,7 +66,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(SSLSocketChannel2.class);
++  private final Logger log = 
Logger.getLogger(SSLSocketChannel2.class.getName());
+ 
+   protected ExecutorService exec;
+ 
+@@ -256,13 +256,13 @@
+         inCrypt = ByteBuffer.allocate(netBufferMax);
+       }
+     }
+-    if (inData.remaining() != 0 && log.isTraceEnabled()) {
+-      log.trace(new String(inData.array(), inData.position(), 
inData.remaining()));
++    if (inData.remaining() != 0 && log.isLoggable(Level.FINE)) {
++      log.fine(new String(inData.array(), inData.position(), 
inData.remaining()));
+     }
+     inData.rewind();
+     inData.flip();
+-    if (inCrypt.remaining() != 0 && log.isTraceEnabled()) {
+-      log.trace(new String(inCrypt.array(), inCrypt.position(), 
inCrypt.remaining()));
++    if (inCrypt.remaining() != 0 && log.isLoggable(Level.FINE)) {
++      log.fine(new String(inCrypt.array(), inCrypt.position(), 
inCrypt.remaining()));
+     }
+     inCrypt.rewind();
+     inCrypt.flip();
+@@ -498,4 +498,4 @@
+       saveCryptData = null;
+     }
+   }
+-}
+\ No newline at end of file
++}
+diff -ru a/src/main/java/org/java_websocket/SSLSocketChannel.java 
b/src/main/java/org/java_websocket/SSLSocketChannel.java
+--- a/src/main/java/org/java_websocket/SSLSocketChannel.java   2023-07-20 
21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/SSLSocketChannel.java   2023-08-30 
11:55:09.427244528 +0100
+@@ -39,8 +39,7 @@
+ import javax.net.ssl.SSLSession;
+ import org.java_websocket.interfaces.ISSLChannel;
+ import org.java_websocket.util.ByteBufferUtils;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
++import java.util.logging.Logger;
+ 
+ 
+ /**
+@@ -70,7 +69,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(SSLSocketChannel.class);
++  private final Logger log = 
Logger.getLogger(SSLSocketChannel.class.getName());
+ 
+   /**
+    * The underlying socket channel
+@@ -148,7 +147,7 @@
+       try {
+         socketChannel.close();
+       } catch (IOException e) {
+-        log.error("Exception during the closing of the channel", e);
++        log.severe("Exception during the closing of the channel" + " : " + e);
+       }
+     }
+   }
+@@ -176,7 +175,7 @@
+         try {
+           result = engine.unwrap(peerNetData, peerAppData);
+         } catch (SSLException e) {
+-          log.error("SSLException during unwrap", e);
++          log.severe("SSLException during unwrap" + " : " + e);
+           throw e;
+         }
+         switch (result.getStatus()) {
+@@ -490,7 +489,7 @@
+     try {
+       engine.closeInbound();
+     } catch (Exception e) {
+-      log.error(
++      log.severe(
+           "This engine was forced to close inbound, without having received 
the proper SSL/TLS close notification message from the peer, due to end of 
stream.");
+     }
+     closeConnection();
+@@ -536,4 +535,4 @@
+   public SSLEngine getSSLEngine() {
+     return engine;
+   }
+-}
+\ No newline at end of file
++}
+diff -ru a/src/main/java/org/java_websocket/WebSocketImpl.java 
b/src/main/java/org/java_websocket/WebSocketImpl.java
+--- a/src/main/java/org/java_websocket/WebSocketImpl.java      2023-07-20 
21:24:05.000000000 +0100
++++ b/src/main/java/org/java_websocket/WebSocketImpl.java      2023-08-30 
12:12:26.045577651 +0100
+@@ -61,8 +61,8 @@
+ import org.java_websocket.protocols.IProtocol;
+ import org.java_websocket.server.WebSocketServer.WebSocketWorker;
+ import org.java_websocket.util.Charsetfunctions;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
++import java.util.logging.Level;
++import java.util.logging.Logger;
+ 
+ /**
+  * Represents one end (client or server) of a single WebSocketImpl 
connection. Takes care of the
+@@ -95,7 +95,7 @@
+    *
+    * @since 1.4.0
+    */
+-  private final Logger log = LoggerFactory.getLogger(WebSocketImpl.class);
++  private final Logger log = Logger.getLogger(WebSocketImpl.class.getName());
+ 
+   /**
+    * Queue of buffers that need to be sent to the client.
+@@ -224,8 +224,8 @@
+    */
+   public void decode(ByteBuffer socketBuffer) {
+     assert (socketBuffer.hasRemaining());
+-    if (log.isTraceEnabled()) {
+-      log.trace("process({}): ({})", socketBuffer.remaining(),
++    if (log.isLoggable(Level.FINE)) {
++      log.fine("process({}): ({})" + " : " + socketBuffer.remaining() + " : " 
+
+               (socketBuffer.remaining() > 1000 ? "too big to display"
+                       : new String(socketBuffer.array(), 
socketBuffer.position(), socketBuffer.remaining())));
+     }
+@@ -280,7 +280,7 @@
+                 socketBuffer.reset();
+                 Handshakedata tmphandshake = 
d.translateHandshake(socketBuffer);
+                 if (!(tmphandshake instanceof ClientHandshake)) {
+-                  log.trace("Closing due to wrong handshake");
++                  log.fine("Closing due to wrong handshake");
+                   closeConnectionDueToWrongHandshake(
+                       new InvalidDataException(CloseFrame.PROTOCOL_ERROR, 
"wrong http function"));
+                   return false;
+@@ -293,11 +293,11 @@
+                   try {
+                     response = wsl.onWebsocketHandshakeReceivedAsServer(this, 
d, handshake);
+                   } catch (InvalidDataException e) {
+-                    log.trace("Closing due to wrong handshake. Possible 
handshake rejection", e);
++                    log.fine("Closing due to wrong handshake. Possible 
handshake rejection" + " : " + e);
+                     closeConnectionDueToWrongHandshake(e);
+                     return false;
+                   } catch (RuntimeException e) {
+-                    log.error("Closing due to internal server error", e);
++                    log.severe("Closing due to internal server error" + " : " 
+ e);
+                     wsl.onWebsocketError(this, e);
+                     closeConnectionDueToInternalServerError(e);
+                     return false;
+@@ -313,7 +313,7 @@
+               }
+             }
+             if (draft == null) {
+-              log.trace("Closing due to protocol error: no draft matches");
++              log.fine("Closing due to protocol error: no draft matches");
+               closeConnectionDueToWrongHandshake(
+                   new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "no 
draft matches"));
+             }
+@@ -322,7 +322,7 @@
+             // special case for multiple step handshakes
+             Handshakedata tmphandshake = 
draft.translateHandshake(socketBuffer);
+             if (!(tmphandshake instanceof ClientHandshake)) {
+-              log.trace("Closing due to protocol error: wrong http function");
++              log.fine("Closing due to protocol error: wrong http function");
+               flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", 
false);
+               return false;
+             }
+@@ -333,7 +333,7 @@
+               open(handshake);
+               return true;
+             } else {
+-              log.trace("Closing due to protocol error: the handshake did 
finally not match");
++              log.fine("Closing due to protocol error: the handshake did 
finally not match");
+               close(CloseFrame.PROTOCOL_ERROR, "the handshake did finally not 
match");
+             }
+             return false;
+@@ -342,7 +342,7 @@
+           draft.setParseMode(role);
+           Handshakedata tmphandshake = draft.translateHandshake(socketBuffer);
+           if (!(tmphandshake instanceof ServerHandshake)) {
+-            log.trace("Closing due to protocol error: wrong http function");
++            log.fine("Closing due to protocol error: wrong http function");
+             flushAndClose(CloseFrame.PROTOCOL_ERROR, "wrong http function", 
false);
+             return false;
+           }
+@@ -352,11 +352,11 @@
+             try {
+               wsl.onWebsocketHandshakeReceivedAsClient(this, 
handshakerequest, handshake);
+             } catch (InvalidDataException e) {
+-              log.trace("Closing due to invalid data exception. Possible 
handshake rejection", e);
++              log.fine("Closing due to invalid data exception. Possible 
handshake rejection" + " : " + e);
+               flushAndClose(e.getCloseCode(), e.getMessage(), false);
+               return false;
+             } catch (RuntimeException e) {
+-              log.error("Closing since client was never connected", e);
++              log.severe("Closing since client was never connected" + " : " + 
e);
+               wsl.onWebsocketError(this, e);
+               flushAndClose(CloseFrame.NEVER_CONNECTED, e.getMessage(), 
false);
+               return false;
+@@ -364,12 +364,12 @@
+             open(handshake);
+             return true;
+           } else {
+-            log.trace("Closing due to protocol error: draft {} refuses 
handshake", draft);
++            log.fine("Closing due to protocol error: draft {} refuses 
handshake" + " : " + draft);
+             close(CloseFrame.PROTOCOL_ERROR, "draft " + draft + " refuses 
handshake");
+           }
+         }
+       } catch (InvalidHandshakeException e) {
+-        log.trace("Closing due to invalid handshake", e);
++        log.fine("Closing due to invalid handshake" + " : " + e);
+         close(e);
+       }
+     } catch (IncompleteHandshakeException e) {
+@@ -398,24 +398,24 @@
+     try {
+       frames = draft.translateFrame(socketBuffer);
+       for (Framedata f : frames) {
+-        log.trace("matched frame: {}", f);
++        log.fine("matched frame: {}" + " : " + f);
+         draft.processFrame(this, f);
+       }
+     } catch (LimitExceededException e) {
+       if (e.getLimit() == Integer.MAX_VALUE) {
+-        log.error("Closing due to invalid size of frame", e);
++        log.severe("Closing due to invalid size of frame" + " : " + e);
+         wsl.onWebsocketError(this, e);
+       }
+       close(e);
+     } catch (InvalidDataException e) {
+-      log.error("Closing due to invalid data in frame", e);
++      log.severe("Closing due to invalid data in frame" + " : " + e);
+       wsl.onWebsocketError(this, e);
+       close(e);
+     } catch (VirtualMachineError | ThreadDeath | LinkageError e) {
+-      log.error("Got fatal error during frame processing");
++      log.severe("Got fatal error during frame processing");
+       throw e;
+     } catch (Error e) {
+-      log.error("Closing web socket due to an error during frame processing");
++      log.severe("Closing web socket due to an error during frame 
processing");
+       Exception exception = new Exception(e);
+       wsl.onWebsocketError(this, exception);
+       String errorMessage = "Got error " + e.getClass().getName();
+@@ -491,7 +491,7 @@
+               sendFrame(closeFrame);
+             }
+           } catch (InvalidDataException e) {
+-            log.error("generated frame is invalid", e);
++            log.severe("generated frame is invalid" + " : " + e);
+             wsl.onWebsocketError(this, e);
+             flushAndClose(CloseFrame.ABNORMAL_CLOSE, "generated frame is 
invalid", false);
+           }
+@@ -551,9 +551,9 @@
+         channel.close();
+       } catch (IOException e) {
+         if (e.getMessage() != null && e.getMessage().equals("Broken pipe")) {
+-          log.trace("Caught IOException: Broken pipe during 
closeConnection()", e);
++          log.fine("Caught IOException: Broken pipe during closeConnection()" 
+ " : " + e);
+         } else {
+-          log.error("Exception during channel.close()", e);
++          log.severe("Exception during channel.close()" + " : " + e);
+           wsl.onWebsocketError(this, e);
+         }
+       }
+@@ -601,7 +601,7 @@
+     try {
+       wsl.onWebsocketClosing(this, code, message, remote);
+     } catch (RuntimeException e) {
+-      log.error("Exception in onWebsocketClosing", e);
++      log.severe("Exception in onWebsocketClosing" + " : " + e);
+       wsl.onWebsocketError(this, e);
+     }
+     if (draft != null) {
+@@ -678,7 +678,7 @@
+     }
+     ArrayList<ByteBuffer> outgoingFrames = new ArrayList<>();
+     for (Framedata f : frames) {
+-      log.trace("send frame: {}", f);
++      log.fine("send frame: {}" + " : " + f);
+       outgoingFrames.add(draft.createBinaryFrame(f));
+     }
+     write(outgoingFrames);
+@@ -729,7 +729,7 @@
+       // Stop if the client code throws an exception
+       throw new InvalidHandshakeException("Handshake data rejected by 
client.");
+     } catch (RuntimeException e) {
+-      log.error("Exception in startHandshake", e);
++      log.severe("Exception in startHandshake" + " : " + e);
+       wsl.onWebsocketError(this, e);
+       throw new InvalidHandshakeException("rejected because of " + e);
+     }
+@@ -739,8 +739,8 @@
+   }
+ 
+   private void write(ByteBuffer buf) {
+-    log.trace("write({}): {}", buf.remaining(),
+-        buf.remaining() > 1000 ? "too big to display" : new 
String(buf.array()));
++    log.fine("write({}): {}" + " : " + buf.remaining() + " : " +
++        (buf.remaining() > 1000 ? "too big to display" : new 
String(buf.array())));
+ 
+     outQueue.add(buf);
+     wsl.onWriteDemand(this);
+@@ -760,7 +760,7 @@
+   }
+ 
+   private void open(Handshakedata d) {
+-    log.trace("open using draft: {}", draft);
++    log.fine("open using draft: {}" + " : " + draft);
+     readyState = ReadyState.OPEN;
+     updateLastPong();
+     try {
diff --git a/include/LibreOfficeKit/LibreOfficeKit.h 
b/include/LibreOfficeKit/LibreOfficeKit.h
index ba51c5afeea9..5e77e30549d0 100644
--- a/include/LibreOfficeKit/LibreOfficeKit.h
+++ b/include/LibreOfficeKit/LibreOfficeKit.h
@@ -131,6 +131,15 @@ struct _LibreOfficeKitClass
     /// @see lok::Office::trimMemory
     /// @since LibreOffice 7.6
     void (*trimMemory) (LibreOfficeKit* pThis, int nTarget);
+
+    /// @see lok::Office::startURP
+    int (*startURP)(LibreOfficeKit* pThis, void* pReceiveURPFromLOContext,
+                    void** ppSendURPToLOContext,
+                    int (*fnReceiveURPFromLO)(void* pContext, const signed 
char* pBuffer, int nLen),
+                    int (**pfnSendURPToLO)(void* pContext, const signed char* 
pBuffer, int nLen));
+
+    /// @see lok::Office::stopURP
+    void (*stopURP)(LibreOfficeKit* pThis, void* pSendURPToLOContext);
 };
 
 #define LIBREOFFICEKIT_DOCUMENT_HAS(pDoc,member) 
LIBREOFFICEKIT_HAS_MEMBER(LibreOfficeKitDocumentClass,member,(pDoc)->pClass->nSize)
diff --git a/include/LibreOfficeKit/LibreOfficeKit.hxx 
b/include/LibreOfficeKit/LibreOfficeKit.hxx
index 65f263c368a6..ff3974417c69 100644
--- a/include/LibreOfficeKit/LibreOfficeKit.hxx
+++ b/include/LibreOfficeKit/LibreOfficeKit.hxx
@@ -1166,6 +1166,32 @@ public:
     {
         mpThis->pClass->trimMemory(mpThis, nTarget);
     }
+
+    /**
+     * Start a UNO acceptor using the function pointers provides to read and 
write data to/from the acceptor.
+     *
+     * @param pReceiveURPFromLOContext A pointer that will be passed to your 
fnRecieveURPFromLO function
+     * @param ppSendURPToLOContext A pointer to a pointer that you should give 
to the function passing URP to LO will be stored in this.
+     * @param fnReceiveURPFromLO A function pointer that LO should use to pass 
URP back to the caller
+     * @param pfnSendURPToLO A function pointer pointer that the caller should 
use to pass URP to LO will be stored in this.
+     */
+    bool startURP(void* pReceiveURPFromLOContext, void** ppSendURPToLOContext,
+                  int (*fnReceiveURPFromLO)(void* pContext, const signed char* 
pBuffer, int nLen),
+                  int (**pfnSendURPToLO)(void* pContext, const signed char* 
pBuffer, int nLen))
+    {
+        return mpThis->pClass->startURP(mpThis, pReceiveURPFromLOContext, 
ppSendURPToLOContext,
+                                        fnReceiveURPFromLO, pfnSendURPToLO);
+    }
+
+    /**
+     * Stop a function based URP connection you previously started with 
startURP
+     *
+     * @param pSendURPToLOContext the context you got back in the 
ppSendURPToLOContext argument when starting the connection
+     */
+    void stopURP(void* pSendURPToLOContext)
+    {
+        mpThis->pClass->stopURP(mpThis, pSendURPToLOContext);
+    }
 };
 
 /// Factory method to create a lok::Office instance.
diff --git a/include/sal/log-areas.dox b/include/sal/log-areas.dox
index 646c7b3d58a2..5540c0d2f228 100644
--- a/include/sal/log-areas.dox
+++ b/include/sal/log-areas.dox
@@ -328,6 +328,7 @@ certain functionality.
 @li @c lok.tiledrendering
 @li @c lok.dialog
 @li @c lok.a11y - LOK accessibility
+@li @c lok.urp - Uno Remote Protocol
 
 @section l10ntools
 
diff --git a/readlicense_oo/license/license.xml 
b/readlicense_oo/license/license.xml
index 637f85d7239c..57d270e15b94 100644
--- a/readlicense_oo/license/license.xml
+++ b/readlicense_oo/license/license.xml
@@ -253,6 +253,24 @@
         ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
CONNECTION WITH THE SOFTWARE OR THE USE OR
         OTHER DEALINGS IN THE SOFTWARE.</p>
     </div>
+    <div class="JAVA_WEBSOCKET">
+    <h2>Java WebSocket</h2>
+        <p>The following software may be included in this product: Java 
WebSocket. Use of any of this software is governed
+        by the terms of the license below:</p>
+        <h3>The MIT License</h3>
+        <p>Copyright (c) 2010-2020 Nathan Rajlich</p>
+        <p>Permission is hereby granted, free of charge, to any person 
obtaining a copy of this software and associated
+        documentation files (the "Software"), to deal in the Software without 
restriction, including without limitation
+        the rights to use, copy, modify, merge, publish, distribute, 
sublicense, and/or sell copies of the Software,
+        and to permit persons to whom the Software is furnished to do so, 
subject to the following conditions:</p>
+        <p>The above copyright notice and this permission notice shall be 
included in all copies or substantial
+        portions of the Software.</p>
+        <p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
EXPRESS OR IMPLIED, INCLUDING BUT NOT
+        LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+        SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+        ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
CONNECTION WITH THE SOFTWARE OR THE USE OR
+        OTHER DEALINGS IN THE SOFTWARE.</p>
+    </div>
     <div class="JFREEREPORT">
     <h2>Flute</h2>
         <p>The following software may be included in this product: Flute. Use 
of any of this software is governed by the
diff --git a/ridljar/Jar_libreoffice.mk b/ridljar/Jar_libreoffice.mk
index d34ae3f5ebda..76a56eedc078 100644
--- a/ridljar/Jar_libreoffice.mk
+++ b/ridljar/Jar_libreoffice.mk
@@ -18,11 +18,16 @@ $(eval $(call gb_Jar_use_jars,libreoffice, \
     unoloader \
 ))
 
+$(eval $(call gb_Jar_use_externals,libreoffice,\
+    java_websocket \
+))
+
 $(eval $(call gb_Jar_set_packageroot,libreoffice,com))
 
 $(eval $(call gb_Jar_set_manifest,libreoffice,$(SRCDIR)/ridljar/util/manifest))
 
 $(eval $(call gb_Jar_add_manifest_classpath,libreoffice, \
+    java_websocket.jar \
     unoloader.jar \
     $(if $(filter MACOSX,$(OS)),../../Frameworks/,../) \
 ))
@@ -63,6 +68,9 @@ $(eval $(call gb_Jar_add_sourcefiles,libreoffice,\
     ridljar/com/sun/star/lib/connections/socket/SocketConnection \
     ridljar/com/sun/star/lib/connections/socket/socketAcceptor \
     ridljar/com/sun/star/lib/connections/socket/socketConnector \
+    ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor \
+    ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection \
+    ridljar/com/sun/star/lib/connections/websocket/websocketConnector \
     ridljar/com/sun/star/lib/uno/Proxy \
     ridljar/com/sun/star/lib/uno/adapter/ByteArrayToXInputStreamAdapter \
     ridljar/com/sun/star/lib/uno/adapter/InputStreamToXInputStreamAdapter \
diff --git a/ridljar/com/sun/star/comp/helper/Bootstrap.java 
b/ridljar/com/sun/star/comp/helper/Bootstrap.java
index edf21d7d2adf..6b371f50324b 100644
--- a/ridljar/com/sun/star/comp/helper/Bootstrap.java
+++ b/ridljar/com/sun/star/comp/helper/Bootstrap.java
@@ -32,6 +32,7 @@ import com.sun.star.lib.util.NativeLibraryLoader;
 import com.sun.star.loader.XImplementationLoader;
 import com.sun.star.uno.UnoRuntime;
 import com.sun.star.uno.XComponentContext;
+import com.sun.star.beans.XPropertySet;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -397,6 +398,68 @@ public class Bootstrap {
         return xContext;
     }
 
+    /**
+     * Bootstraps the component context from a websocket location.
+     *
+     * @param url
+     *        the ws:// or wss:// url of the websocket server
+     *
+     * @throws BootstrapException if things go awry.
+     *
+     * @return a bootstrapped component context.
+     *
+     * @since LibreOffice 24.2
+     */
+    public static final XComponentContext bootstrapWebsocketConnection( String 
url )
+        throws BootstrapException {
+
+        XComponentContext xContext = null;
+
+        try {
+            // create default local component context
+            XComponentContext xLocalContext =
+                createInitialComponentContext( (Map<String, Object>) null );
+            if ( xLocalContext == null )
+                throw new BootstrapException( "no local component context!" );
+
+            // initial service manager
+            XMultiComponentFactory xLocalServiceManager =
+                xLocalContext.getServiceManager();
+            if ( xLocalServiceManager == null )
+                throw new BootstrapException( "no initial service manager!" );
+
+            // create a URL resolver
+            XUnoUrlResolver xUrlResolver =
+                UnoUrlResolver.create( xLocalContext );
+
+            // connection string
+            String sConnect = "uno:websocket"
+                              + ",url=" + url
+                              + ";urp;StarOffice.ComponentContext";
+
+            try {
+                // try to connect to office
+                Object xOfficeServiceManager = xUrlResolver.resolve(sConnect);
+
+                xContext = UnoRuntime.queryInterface(XComponentContext.class, 
xOfficeServiceManager);
+
+                if ( xContext == null )
+                    throw new BootstrapException( "no component context!" );
+            } catch ( com.sun.star.connection.NoConnectException ex ) {
+                throw new BootstrapException(ex);
+            }
+        } catch ( BootstrapException e ) {
+            throw e;
+        } catch ( java.lang.RuntimeException e ) {
+            throw e;
+        } catch ( java.lang.Exception e ) {
+            throw new BootstrapException( e );
+        }
+
+        return xContext;
+    }
+
+
     private static final Random randomPipeName = new Random();
 
     private static void pipe(
diff --git 
a/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java 
b/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java
new file mode 100644
index 000000000000..439a52551726
--- /dev/null
+++ b/ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java
@@ -0,0 +1,60 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- 
*/
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   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 .
+ */
+
+package com.sun.star.lib.connections.websocket;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Helper class for <code>websocketConnector</code>.
+ */
+final class ConnectionDescriptor {
+    public ConnectionDescriptor(String description)
+    throws com.sun.star.lang.IllegalArgumentException {
+        Iterator<String> descriptionParts = 
Arrays.stream(description.split(",")).iterator();
+        descriptionParts
+            .next(); // skip over the first part as it's the protocol not a 
real parameter
+        while (descriptionParts.hasNext())
+        {
+            String parameter = descriptionParts.next();
+            String[] pair = parameter.split("=", 2);
+
+            if (pair.length != 2)
+            {
+                throw new com.sun.star.lang.IllegalArgumentException(
+                    String.format("parameter %s lacks '='", parameter));
+            }
+
+            String key = pair[0];
+            String value = pair[1];
+            if (key.equalsIgnoreCase("url")) {
+                url = value;
+            }
+        }
+    }
+
+    public String getURL() {
+        return url;
+    }
+
+    private String url = null;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git 
a/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java 
b/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java
new file mode 100644
index 000000000000..7f522df409d1
--- /dev/null
+++ b/ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java
@@ -0,0 +1,335 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- 
*/
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   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 .
+ */
+package com.sun.star.lib.connections.websocket;
+
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.ProtocolException;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import javax.swing.text.html.HTMLDocument.Iterator;
+
+import com.sun.star.connection.XConnection;
+import com.sun.star.connection.XConnectionBroadcaster;
+import com.sun.star.io.XStreamListener;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
+/**
+ * The WebsocketConnection implements the <code>XConnection</code> interface
+ * and is uses by the <code>WebsocketConnector</code>.
+ *
+ * <p>This class is not part of the provided <code>api</code>.</p>
+ *
+ * @see         com.sun.star.lib.connections.socket.socketAcceptor
+ * @see         com.sun.star.lib.connections.socket.socketConnector
+ * @see         com.sun.star.connection.XConnection
+ */
+public class WebsocketConnection extends WebSocketClient implements 
XConnection, XConnectionBroadcaster {
+    /**
+     * When set to true, enables various debugging output.
+     */
+    public static final boolean DEBUG = false;
+    static final byte[] outgoingPrefix = { 'u', 'r', 'p', ' ' };
+
+    protected String       _description;
+    protected InputStream  _inputStream;
+    protected OutputStream _outputStream;
+
+    protected InputStream _outputStreamReader;
+    protected OutputStream _inputStreamWriter;
+
+    protected ArrayList<XStreamListener>       _listeners;
+
+    /**
+     * Constructs a new <code>WebsocketConnection</code>.
+     *
+     * @param  description   the description of the connection.
+     * @param  desc          the websocket ConnectionDescriptor containing 
information such as the websocket URL
+     */
+    public WebsocketConnection(String description, ConnectionDescriptor desc) 
throws IOException, URISyntaxException, InterruptedException {
+        super(new URI(desc.getURL()));
+
+        if (DEBUG) System.err.println("##### " + getClass().getName() + " - 
instantiated " + description + " " + desc);
+
+        _description = description;
+
+        PipedOutputStream inputStreamWriter = new PipedOutputStream();
+        PipedInputStream inputPipe = new PipedInputStream(inputStreamWriter);
+        PipedOutputStream outputPipe = new PipedOutputStream();
+        PipedInputStream outputStreamReader = new PipedInputStream(outputPipe);
+
+
+        _inputStream = new BufferedInputStream(inputPipe);
+        _inputStreamWriter = inputStreamWriter;
+        _outputStream = new BufferedOutputStream(outputPipe);
+        _outputStreamReader = outputStreamReader;
+
+        _listeners = new ArrayList<XStreamListener>();
+
+        connectBlocking();
+    }
+
+    public void addStreamListener(XStreamListener aListener )
+    throws com.sun.star.uno.RuntimeException {
+        _listeners.add(aListener);
+    }
+
+    public void removeStreamListener(XStreamListener aListener )
+    throws com.sun.star.uno.RuntimeException {
+        _listeners.remove(aListener);
+    }
+
+    private void notifyListeners_open() {
+        for (XStreamListener xStreamListener : _listeners) {
+            xStreamListener.started();
+        }
+    }
+
+    private void notifyListeners_close() {
+        for (XStreamListener xStreamListener : _listeners) {
+            xStreamListener.closed();
+        }
+    }
+
+    private void notifyListeners_error(com.sun.star.uno.Exception exception) {
+        for (XStreamListener xStreamListener : _listeners) {
+            xStreamListener.error(exception);
+        }
+    }
+
+    /**
+     * Read the required number of bytes.
+     *
+     * @param    bytes   the outparameter, where the bytes have to be placed.
+     * @param    nBytesToRead the number of bytes to read.
+     * @return   the number of bytes read.
+     *
+     * @see      com.sun.star.connection.XConnection#read
+     */
+    public int read(/*OUT*/byte[][] bytes, int nBytesToRead)
+    throws com.sun.star.io.IOException, com.sun.star.uno.RuntimeException {
+
+        String errMessage = null;
+
+        int read_bytes = 0;
+        bytes[0] = new byte[nBytesToRead];
+
+        try {
+            _inputStreamWriter.flush();
+
+            int count  ;
+
+            do {
+                count = _inputStream.read(bytes[0], read_bytes, nBytesToRead - 
read_bytes);
+                if(count == -1)
+                    errMessage = "EOF reached - " + getDescription();
+
+                read_bytes += count;
+            }
+            while(read_bytes >= 0 && read_bytes < nBytesToRead && count >= 0);
+        } catch(IOException ioException) {
+            if(DEBUG) {
+                System.err.println("##### " + getClass().getName() + ".read - 
exception occurred:" + ioException);
+                ioException.printStackTrace();
+            }
+
+            errMessage = ioException.toString();
+        }
+
+        if(errMessage != null) {
+            com.sun.star.io.IOException unoIOException = new 
com.sun.star.io.IOException(errMessage);
+            notifyListeners_error(unoIOException);
+
+            throw unoIOException;
+        }
+
+        if (DEBUG) System.err.println(String.format("##### %s - read %s bytes 
of %s requested", getClass().getName(), Integer.toString(read_bytes), 
Integer.toString(nBytesToRead)));
+
+        return read_bytes;
+    }
+
+    /**
+     * Write bytes.
+     *
+     * @param    aData the bytes to write.
+     * @see       com.sun.star.connection.XConnection#write
+     */
+    public void write(byte aData[]) throws com.sun.star.io.IOException,
+        com.sun.star.uno.RuntimeException {
+        try {
+            _outputStream.write(aData);
+        } catch(IOException ioException) {
+            com.sun.star.io.IOException unoIOException = new 
com.sun.star.io.IOException(ioException);
+            notifyListeners_error(unoIOException);
+
+            throw unoIOException;
+        }
+
+        if (DEBUG) System.err.println(String.format("##### %s - wrote %s 
bytes", getClass().getName(), Integer.toString(aData.length)));
+    }
+
+    /**
+     * Sends the data over the websocket to whatever is on the other side.
+     *
+     * **NOTE**: unlike with genuine streams, without flushing the data is
+     *           never sent
+     *
+     * @see       com.sun.star.connection.XConnection#flush
+     */
+    public void flush() throws com.sun.star.io.IOException,
+        com.sun.star.uno.RuntimeException {
+        try {
+            _outputStream.flush();
+
+            Integer available = _outputStreamReader.available();
+
+            byte[] outputBytes = new byte[available + outgoingPrefix.length];
+            System.arraycopy(outgoingPrefix, 0, outputBytes, 0, 
outgoingPrefix.length);
+
+            _outputStreamReader.read(outputBytes, outgoingPrefix.length, 
available);
+
+            send(outputBytes);
+        } catch(IOException ioException) {
+            com.sun.star.io.IOException unoIOException = new 
com.sun.star.io.IOException(ioException);
+            notifyListeners_error(unoIOException);
+
+            throw unoIOException;
+        }
+
+        if (DEBUG)
+            System.err.println(String.format("##### %s - flushed", 
getClass().getName()));
+    }
+
+    /**
+     * Closes the connection.
+     *
+     * @see       com.sun.star.connection.XConnection#close
+     */
+    public void close() throws com.sun.star.uno.RuntimeException {
+        if (DEBUG) System.err.println("##### " + getClass().getName() + " - 
socket closed");
+    }
+
+    /**
+     * Gives a description of the connection.
+     *
+     * @return  the description.
+     * @see       com.sun.star.connection.XConnection#getDescription
+     */
+    public String getDescription() throws com.sun.star.uno.RuntimeException {
+        return _description;
+    }
+
+    @Override
+    public void onOpen(ServerHandshake handshakedata) {
+        notifyListeners_open();
+    }
+
+    @Override
+    public void onClose(int code, String reason, boolean remote) {
+        notifyListeners_close();
+    }
+
+    @Override
+    public void onMessage(String message) {
+        String[] messageParts = message.split(": ", 2);
+        if (messageParts.length != 2)
+        {
+            notifyListeners_error(new com.sun.star.uno.Exception(new 
ProtocolException(String.format("Recieved URP/WS message (%s) without a type 
specifier. Messages must be proceeded by 'urp: '", message))));
+            return;
+        }
+
+        String messageType = messageParts[0];
+
+        if (!messageType.equals("urp"))
+        {
+            if (DEBUG) System.err.println(String.format("##### %s - received 
%s message but that is not URP", getClass().getName(), messageType));
+            return;
+        }
+
+        byte[] messageBytes = messageParts[1].getBytes();
+
+        try {
+            _inputStreamWriter.write(messageBytes);
+        } catch (IOException e) {
+            notifyListeners_error(new com.sun.star.uno.Exception(e));
+            return;
+        }
+
+        if (DEBUG) System.err.println(String.format("##### %s - recieved %s 
chars", getClass().getName(), Integer.toString(messageBytes.length)));
+    }
+
+    @Override
+    public void onMessage(ByteBuffer message) {
+        byte[] prefixedMessageBytes = message.array();
+
+        String messageType = "";
+        boolean hasType = false;
+        int i;
+        for (i = 0; i < prefixedMessageBytes.length - 1; i++) {
+            if (prefixedMessageBytes[i] == ':' && (prefixedMessageBytes[i+1] 
== ' ' || prefixedMessageBytes[i+1] == '\n')) {
+                hasType = true;
+                break;  // The type ends with ": ", so if we find this 
sequence we found the end of our type
+            }
+            messageType += (char)prefixedMessageBytes[i];
+        }
+
+        if(!hasType) {
+            notifyListeners_error(new com.sun.star.uno.Exception(new 
ProtocolException(String.format("Recieved URP/WS message (%s) without a type 
specifier. Binary messages must be proceeded by 'urp: ' or 'urp:\\n'", 
message))));
+            return;
+        }
+
+        int messageStartIndex = i + 2;
+
+        if (!messageType.equals("urp")) {
+            if (DEBUG) System.err.println(String.format("##### %s - recieved 
%s binary message but that is not URP", getClass().getName(), messageType));
+            return;
+        }
+
+        byte[] messageBytes = Arrays.copyOfRange(prefixedMessageBytes, 
messageStartIndex, prefixedMessageBytes.length);
+
+        try {
+            _inputStreamWriter.write(messageBytes);
+        } catch (IOException e) {
+            notifyListeners_error(new com.sun.star.uno.Exception(e));
+            return;
+        }
+
+        if (DEBUG) System.err.println(String.format("##### %s - recieved %s 
bytes", getClass().getName(), Integer.toString(prefixedMessageBytes.length)));
+    }
+
+    @Override
+    public void onError(Exception ex) {
+        notifyListeners_error(new com.sun.star.uno.Exception(ex));
+    }
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git 
a/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java 
b/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java
new file mode 100644
index 000000000000..a40bb0093c4d
--- /dev/null
+++ b/ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java
@@ -0,0 +1,137 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- 
*/
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   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 .
+ */
+
+package com.sun.star.lib.connections.websocket;
+
+import com.sun.star.comp.loader.FactoryHelper;
+import com.sun.star.connection.ConnectionSetupException;
+import com.sun.star.connection.NoConnectException;
+import com.sun.star.connection.XConnection;
+import com.sun.star.connection.XConnector;
+import com.sun.star.lang.XMultiServiceFactory;
+import com.sun.star.lang.XSingleServiceFactory;
+import com.sun.star.registry.XRegistryKey;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+
+/**
+ * A component that implements the <code>XConnector</code> interface.
+ *
+ * <p>The <code>websocketConnector</code> is a specialized component that uses
+ * websockets for communication.  The <code>websocketConnector</code> is 
generally
+ * used by the <code>com.sun.star.connection.Connector</code> service.</p>
+ *
+ * @see com.sun.star.connection.XAcceptor
+ * @see com.sun.star.connection.XConnection
+ * @see com.sun.star.connection.XConnector
+ * @see com.sun.star.comp.loader.JavaLoader
+ */
+public final class websocketConnector implements XConnector {
+    /**
+     * The name of the service.
+     *
+     * <p>The <code>JavaLoader</code> accesses this through reflection.</p>
+     *
+     * @see com.sun.star.comp.loader.JavaLoader
+     */
+    public static final String __serviceName
+        = "com.sun.star.connection.websocketConnector";
+
+    /**
+     * Returns a factory for creating the service.
+     *
+     * <p>This method is called by the <code>JavaLoader</code>.</p>
+     *
+     * @param implName the name of the implementation for which a service is
+     *     requested.
+     * @param multiFactory the service manager to be used (if needed).
+     * @param regKey the registry key.
+     * @return an <code>XSingleServiceFactory</code> for creating the 
component.
+     *
+     * @see com.sun.star.comp.loader.JavaLoader
+     */
+    public static XSingleServiceFactory __getServiceFactory(
+        String implName, XMultiServiceFactory multiFactory, XRegistryKey 
regKey)
+    {
+        return implName.equals(websocketConnector.class.getName())
+               ? FactoryHelper.getServiceFactory(websocketConnector.class,
+                       __serviceName, multiFactory,
+                       regKey)
+               : null;
+    }
+
+    /**
+     * Connects via the described websocket to a waiting server.
+     *
+     * <p>The connection description has the following format:
+     * <code><var>type</var></code><!--
+     *     -->*(<code><var>key</var>=<var>value</var></code>),
+     * where <code><var>type</var></code> should be <code>websocket</code>
+     * (ignoring case).  Supported keys (ignoring case) currently are</p>
+     * <dl>
+     * <dt><code>url</code>
+     * <dd>The URL the websocket server is listening on, starting with
+     *     either ws:// or wss://
+     * </dl>
+     *
+     * @param connectionDescription the description of the connection.
+     * @return an <code>XConnection</code> to the server.
+     *
+     * @see com.sun.star.connection.XAcceptor
+     * @see com.sun.star.connection.XConnection
+     */
+    public synchronized XConnection connect(String connectionDescription)
+    throws NoConnectException, ConnectionSetupException
+    {
+        if (connected)
+            throw new ConnectionSetupException("Already connected to the 
socket");
+
+        ConnectionDescriptor desc;
+        try {
+            desc = new ConnectionDescriptor(connectionDescription);
+        } catch (com.sun.star.lang.IllegalArgumentException e) {
+            throw new ConnectionSetupException(e);
+        }
+
+        WebsocketConnection websocket = null;
+        try {
+            websocket = new WebsocketConnection(connectionDescription, desc);
+            connected = websocket.isOpen();
+        } catch (IOException e) {
+            throw new ConnectionSetupException(e);
+        } catch (URISyntaxException e) {
+            throw new ConnectionSetupException(e);
+        } catch (InterruptedException e) {
+            throw new ConnectionSetupException(e);
+        }
+
+        if (websocket == null || !connected)
+            throw new ConnectionSetupException("Could not connect to the 
server. Is it up?");
+
+        return websocket;
+    }
+
+    private boolean connected = false;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/ridljar/source/libreoffice/module-info.java 
b/ridljar/source/libreoffice/module-info.java
index 8d24c7ccb13c..f913597600db 100644
--- a/ridljar/source/libreoffice/module-info.java
+++ b/ridljar/source/libreoffice/module-info.java
@@ -64,6 +64,7 @@ module org.libreoffice.uno
     exports com.sun.star.ldap;
     exports com.sun.star.lib.connections.pipe;
     exports com.sun.star.lib.connections.socket;
+    exports com.sun.star.lib.connections.websocket;
     exports com.sun.star.lib.uno;
     exports com.sun.star.lib.uno.adapter;
     exports com.sun.star.lib.uno.bridges.java_remote;
diff --git a/ridljar/util/manifest b/ridljar/util/manifest
index bb1209f90a22..44eb59ff784b 100644
--- a/ridljar/util/manifest
+++ b/ridljar/util/manifest
@@ -24,6 +24,9 @@ Sealed: true
 Name: com/sun/star/lib/connections/socket/
 Sealed: true
 
+Name: com/sun/star/lib/connections/websocket/
+Sealed: true
+
 Name: com/sun/star/lib/uno/
 Sealed: true
 
diff --git a/ure/source/README b/ure/source/README
index d66b70003681..da003caacd81 100644
--- a/ure/source/README
+++ b/ure/source/README
@@ -74,6 +74,7 @@ ELF platforms (Linux, Solaris, *BSD):
 /opt/libreoffice/ure/lib/libstocserviceslo.so  [private]
 /opt/libreoffice/ure/lib/libuuresolverlo.so  [private]
 /opt/libreoffice/ure/share/java/java_uno.jar  [private]
+/opt/libreoffice/ure/share/java/java_websocket.jar [private]
 /opt/libreoffice/ure/share/misc/javavendors.xml  [private]
 
 Windows:
@@ -132,6 +133,7 @@ Program Files\URE\bin\stocserviceslo.dll  [private]
 Program Files\URE\bin\uuresolverlo.dll  [private]
 Program Files\URE\bin\uwinapi.dll  [private]
 Program Files\URE\java\java_uno.jar  [private]
+Program Files\URE\java\java_websocket.jar [private]
 Program Files\URE\misc\javavendors.xml  [private]
 
 %windir%\assembly\cli_basetypes.dll [GAC]

Reply via email to