From 8e246331f6f71320cfcc8defdd04e756a75f71cf Mon Sep 17 00:00:00 2001
From: Skyler Grey
Date: Fri, 18 Aug 2023 13:30:35 +0000
Subject: [PATCH] Add a FunctionBasedURPConnection and a websocket URP
connector
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/151171
Tested-by: Jenkins
Reviewed-by: Caolán McNamara
---
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 +
.../ExternalPackage_java_websocket.mk | 16 +
.../ExternalProject_java_websocket.mk | 31 +
external/java_websocket/Makefile | 7 +
.../java_websocket/Module_java_websocket.mk | 18 +
external/java_websocket/README | 3 +
.../UnpackedTarball_java_websocket.mk | 21 +
.../java_websocket/patches/ant-build.patch | 26 +
.../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 +
.../com/sun/star/comp/helper/Bootstrap.java | 63 ++
.../websocket/ConnectionDescriptor.java | 60 ++
.../websocket/WebsocketConnection.java | 335 ++++++++
.../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(-)
create mode 100644 external/java_websocket/ExternalPackage_java_websocket.mk
create mode 100644 external/java_websocket/ExternalProject_java_websocket.mk
create mode 100644 external/java_websocket/Makefile
create mode 100644 external/java_websocket/Module_java_websocket.mk
create mode 100644 external/java_websocket/README
create mode 100644 external/java_websocket/UnpackedTarball_java_websocket.mk
create mode 100644 external/java_websocket/patches/ant-build.patch
create mode 100644 external/java_websocket/patches/no-slf4j.patch
create mode 100644 ridljar/com/sun/star/lib/connections/websocket/ConnectionDescriptor.java
create mode 100644 ridljar/com/sun/star/lib/connections/websocket/WebsocketConnection.java
create mode 100644 ridljar/com/sun/star/lib/connections/websocket/websocketConnector.java
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
#include
#include
+#include
#include
#include
@@ -86,6 +87,7 @@
#include
#include
+#include
#include
#include
#include
@@ -108,6 +110,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include
#include
@@ -121,6 +127,7 @@
#include
#include
#include
+#include
#include
#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
+{
+private:
+ css::uno::Reference m_rContext;
+
+public:
+ FunctionBasedURPInstanceProvider(
+ const css::uno::Reference& rxContext);
+
+ // XInstanceProvider
+ virtual css::uno::Reference
+ SAL_CALL getInstance(const OUString& aName) override;
+};
+
+// InstanceProvider
+FunctionBasedURPInstanceProvider::FunctionBasedURPInstanceProvider(
+ const Reference& rxContext)
+ : m_rContext(rxContext)
+{
+}
+
+Reference FunctionBasedURPInstanceProvider::getInstance(const OUString& aName)
+{
+ Reference rInstance;
+
+ if (aName == "StarOffice.ServiceManager")
+ {
+ rInstance.set(m_rContext->getServiceManager());
+ }
+ else if (aName == "StarOffice.ComponentContext")
+ {
+ rInstance = m_rContext;
+ }
+ else if (aName == "StarOffice.NamingService")
+ {
+ Reference 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
+{
+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& aReadBytes,
+ sal_Int32 nBytesToRead) override;
+ virtual void SAL_CALL write(const Sequence& aData) override;
+ virtual void SAL_CALL flush() override;
+ virtual void SAL_CALL close() override;
+ virtual OUString SAL_CALL getDescription() override;
+ void setBridge(Reference);
+ int addClientURPToBuffer(const signed char* pBuffer, int nLen);
+ void* getContext();
+ inline static int g_connectionCount = 0;
+
+private:
+ std::shared_ptr> m_pBuffer;
+ void* m_pRecieveFromLOContext;
+ int (*m_fnReceiveURPFromLO)(void* pContext, const signed char* pBuffer, int nLen);
+ Reference m_URPBridge;
+ std::atomic 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>())
+ , m_pRecieveFromLOContext(pRecieveFromLOContext)
+ , m_fnReceiveURPFromLO(fnRecieveFromLO)
+{
+ g_connectionCount++;
+}
+
+FunctionBasedURPConnection::~FunctionBasedURPConnection()
+{
+ Reference 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(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& 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(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& 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) { 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 connection(
+ new FunctionBasedURPConnection(pRecieveFromLOContext, fnReceiveURPFromLO));
+
+ *pfnSendURPToLO = sendURPToLO;
+ *ppSendToLOContext = connection->getContext();
+
+ Reference xBridgeFactory = css::bridge::BridgeFactory::create(xContext);
+
+ Reference xInstanceProvider(new FunctionBasedURPInstanceProvider(xContext));
+
+ Reference 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(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 @@
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
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 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;
+
+ /**
+ * WebSocketServer 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 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.
+
+
Java WebSocket
+
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:
+
The MIT License
+
Copyright (c) 2010-2020 Nathan Rajlich
+
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:
+
The above copyright notice and this permission notice shall be included in all copies or substantial
+ portions of the Software.
+
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.
+
Flute
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) 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 websocketConnector
.
+ */
+final class ConnectionDescriptor {
+ public ConnectionDescriptor(String description)
+ throws com.sun.star.lang.IllegalArgumentException {
+ Iterator 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 XConnection
interface
+ * and is uses by the WebsocketConnector
.
+ *
+ * This class is not part of the provided api
.
+ *
+ * @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 _listeners;
+
+ /**
+ * Constructs a new WebsocketConnection
.
+ *
+ * @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();
+
+ 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 XConnector
interface.
+ *
+ * The websocketConnector
is a specialized component that uses
+ * websockets for communication. The websocketConnector
is generally
+ * used by the com.sun.star.connection.Connector
service.
+ *
+ * @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.
+ *
+ * The JavaLoader
accesses this through reflection.
+ *
+ * @see com.sun.star.comp.loader.JavaLoader
+ */
+ public static final String __serviceName
+ = "com.sun.star.connection.websocketConnector";
+
+ /**
+ * Returns a factory for creating the service.
+ *
+ * This method is called by the JavaLoader
.
+ *
+ * @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 XSingleServiceFactory
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.
+ *
+ * The connection description has the following format:
+ * type
*(key=value
),
+ * where type
should be websocket
+ * (ignoring case). Supported keys (ignoring case) currently are
+ *
+ * url
+ * - The URL the websocket server is listening on, starting with
+ * either ws:// or wss://
+ *
+ *
+ * @param connectionDescription the description of the connection.
+ * @return an XConnection
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]