OxOffice Online 模組開發

如何透過 OxOffice Online Module SDK 來開發自有的 Module

如何體驗 OxOOL Module?

取得 SDK 的方法

1. 自行編譯 oxool-community 並打包,可以取得 oxool-devel RPM 包

2. 至 GitHub 下載最新版本的 oxool-devel RPM

取得以後請輸入(請自行替換後面的版號)

sudo yum localinstall -y oxool-devel-xx.xxx.rpm

編譯/打包本專案

請先閱讀上面的技術手冊了解取得 oxool-devel 的方法

本地端編譯與測試開發

./autogen.sh

./configure --enable-debug

make -j4

cp src/.libs/libtest.so.0.0.0 PATH_TO_oxool-community/libtest.so

打包

./autogen.sh

./configure

make dist

rpmbuild -ta oxool-module-sample-1.0.0.tar.gz

測試本專案

需搭配 oxool-community 才能掛載此模組並進行 API 測試

執行 oxool-community 以後打開 http://your_ip:9980/loleaflet/dist/admin/sample,並輸入帳密 admin/admin 可以看到這個頁面。

sample_demo_page.PNG

 

接下來請依照下列順序

  1. 選擇一個 ODT 檔案
  2. 修改「插入文字」欄位
  3. 點選開始測試

快速建構一個自己的模組專案

請執行下面指令並輸入新的模組名稱

./create_your_module.sh

執行完後,新的專案會建立於上層目錄。

 

介紹 SDK & Sample Module

模組 SDK 介紹

要寫一個新的模組需要繼承 oxool-devel 的類別 oxoolmodule

並至少要將 handleRequest 實作出來

另外還有幾個延伸功能的選擇性實作

以 Sample Module 為範例來瞭解 SDK 的概念

 

handleRequest(需要實作)

技術要點

  1. 模組處理 request 時需要 fork
  2. URL 第二個路徑為模組的名稱
void sample::handleRequest(std::weak_ptr<StreamSocket> _socket,
                           MemoryInputStream &message,
                           HTTPRequest &request,
                           SocketDisposition &disposition)
{
    Poco::URI requestUri(request.getURI());
    std::vector<std::string> reqPathSegs;
    requestUri.getPathSegments(reqPathSegs);
    std::string method = request.getMethod();
    Process::PID pid = fork();
    if (pid < 0)
    {
        quickHttpRes(_socket,
                     HTTPResponse::HTTP_SERVICE_UNAVAILABLE);
    }
    else if (pid == 0)
    {
        // This would trigger this fork exit after the socket finish write
        // note: exit point is in wsd/LOOLWSD.cpp where ClientRequestDispatcher (SocketHandler)'s performWrites()
        try
        {
            Poco::URI requestUri(request.getURI());
            std::vector<std::string> reqPathSegs;
            requestUri.getPathSegments(reqPathSegs);
            std::string method = request.getMethod();
            if (request.getMethod() == HTTPRequest::HTTP_GET && reqPathSegs.size() == 2)
            {
                quickHttpRes(_socket, HTTPResponse::HTTP_OK);
            }
            else if (request.getMethod() == HTTPRequest::HTTP_POST &&
                     reqPathSegs[1] == "sample")
            { // /lool/sample
                handlesample(_socket, request, message, disposition);
            }
            else
            {
                quickHttpRes(_socket, HTTPResponse::HTTP_NOT_FOUND);
            }
        }
        catch (std::exception &e)
        {
            std::cout << e.what() << "\n";
            logger().notice("[Exception]" + std::string(e.what()));
            quickHttpRes(_socket, HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
        }

        exit_application = true;
    }
    else
    {
        std::cout << "call from parent" << std::endl;
    }
}

 

handleAdmin: 控管後端管理介面(選擇性實作)

技術要點

  1. handleAdmin 會接到來自 WebSocket 解析過後的一串指令
  2. 指令的結構為: <modulename> <action> <data in this format: x,y,z ....>
  3. 這裡的範例是透過管理介面來管理模組的設定檔
std::string sample::handleAdmin(std::string command)
{
    /*
     *command format: module <modulename> <action> <data in this format: x,y,z ....>
     */
    auto tokenOpts = StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM;
    StringTokenizer tokens(command, " ", tokenOpts);
    std::string result = "Module Loss return";
    std::string action = tokens[2];

    // 以 json 字串傳回 sample.xml 
    if (action == "getConfig" && tokens.count() >= 4)
    {
        std::ostringstream oss;
        oss << "settings {\n";
        for (size_t i = 3; i < tokens.count(); i++)
        {
            std::string key = tokens[i];

            if (i > 3)
                oss << ",\n";

            oss << "\"" << key << "\": ";
            // 下列三種 key 是陣列形式

            if (xml_config->has(key))
            {
                std::string p_value = addSlashes(xml_config->getString(key, "")); // 讀取 value, 沒有的話預設為空字串
                std::string p_type = xml_config->getString(key + "[@type]", "");  // 讀取 type, 沒有的話預設為空字串
                if (p_type == "int" || p_type == "uint" || p_type == "bool" ||
                    p_value == "true" || p_value == "false")
                    oss << p_value;
                else
                    oss << "\"" << p_value << "\"";
            }
            else
            {
                oss << "null";
            }
        }
        oss << "\n}\n";
        result = oss.str();
    }
    else if (action == "setConfig" && tokens.count() > 1)
    {
        Poco::JSON::Object::Ptr object;
        if (JsonUtil::parseJSON(command, object))
        {
            for (Poco::JSON::Object::ConstIterator it = object->begin(); it != object->end(); ++it)
            {
                // it->first : key, it->second.toString() : value
                xml_config->setString(it->first, it->second.toString());
            }
            xml_config->save(ConfigFile);
            result = "setConfigOk";
        }
        else
        {
            result = "setConfigNothing";
        }
    }
    else
        result = "No such command for module " + tokens[1];

    return result;
}

getHTML: 返回後端管理頁面的 static resource(選擇性實作)

技術要點

  1. 基本上 getHTML 無須改動
  2. 解析在專案的靜態資源目錄 /<path-to-module>/html/ 的路徑
  3. 詳細的路徑引用請參閱 GitHub/oxool-module-sample 的 admin.html (可以看下一個小節)
std::string sample::getHTMLFile(std::string fileName)
{
    std::string filePath = "";
#ifdef DEV_DIR
    std::string dev_path(DEV_DIR);
    filePath = dev_path + "/html/";
#else
    filePath = "/var/lib/oxool/sample/html/";
#endif
    filePath += fileName;
    return filePath;
}

admin.html: 後端管理頁面範例與解說

技術要點

  1. oxool-community 採用了模板引擎,目前僅支援 {% BLOCK content %} ...TODO... {% END BLOCK %}
  2. 在 script 的 src 的結構須為: <module-name>/<path-in-module's-html>/<filename>

 

{% BLOCK content %}
<script src="sample/js/AdminSocketConfigSettings.js"> </script>
<script>
    if (window.location.protocol == "https:") {
        host = 'wss://' + window.location.host + '%SERVICE_ROOT%/lool/adminws/'
    }
    else {
        host = 'ws://' + window.location.host + '%SERVICE_ROOT%/lool/adminws/'
    }

    var configSocket = Admin.ConfigSettings(host);
</script>

<ul class="nav nav-tabs">
    <li role="presentation" class="active"><a data-toggle="tab" href="#a1" class="tabctl">API 測試</a></li>
    <li role="presentation" class=""><a data-toggle="tab" href="#a2" class="tabctl">設定檔管理</a></li>
    <li role="presentation" class=""><a data-toggle="tab" href="#a3" class="tabctl">版權聲明</a></li>
</ul>
<div class="tab-content" style="height: 100%;">
    ...
    <!--******************************版權聲明******************************-->
    <div id="a3" class="tab-pane" style="width: 100%; padding-top:10px;">
        <pre id="license" class="panel-body" style="border: 0; background-color: transparent;">
        </pre>
    </div>
</div>

<script>
    var getCookie = function (name) {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = cookies[i].trim();
            if (cookie.indexOf(name) === 0) {
                return cookie;
            }
        }
        return '';
    }
    
</script>
{% END BLOCK %}

Module 功能開發 --- 錄製巨集與製作插件

擴充 Module 功能

模組功能開發的主要功能實作可以自由選擇以下但不限

  1. LibreOffice/OxOffice Extension
  2. uno command
  3. python script

此章節我們要以 LibreOffice/OxOffice Extension 來介紹如何快速透過擴充插件來擴充模組的功能

LibreOffice/OxOffice Extension

寫 Extension 有兩種方法,這邊會延伸介紹第二種方法
  1. 熟悉 VBA 並稍微熟悉 LibreOffice 的 StarBasic 即可自由撰寫
  2. 使用 LibreOffice 內建的【錄製巨集】來快速開發
▶如何錄製巨集?

首先啟用錄製巨集的功能,如下圖

record-macro.PNG

然後打開 Writer 文件,並於【工具 > 巨集 > 錄製巨集】來啟動錄製功能,如下圖

record-macro_2.PNG

錄製過程請操作自己想操作的功能,並將錄製結果儲存起來,並透過編輯巨集來看錄製的成果。

record-macro_3.PNG

record-macro_4.PNG

record-macro_5.PNGrecord-macro_6.PNG

record-macro_7.PNG

 

▶如何製作 Extension?

首先打開 LibreOffice 的【工具 > 巨集 > 組織巨集  > LibreOffice Basic】

LO_create_extension.png

下一步點選:【統籌概覽 > 選函示庫 > 選擇要輸出成擴充套件的函示庫 > 確定】

LO_create_extension_2.png

LO_create_extension_3.png

LO_create_extension_4.png

 

 

Module 功能開發 --- Uno Command

擴充 Module 功能

模組功能開發的主要功能實作可以自由選擇以下但不限

  1. LibreOffice/OxOffice Extension
  2. uno command
  3. python script

此章節我們要以 Uno Command 來介紹如何快速透過擴充插件來擴充模組的功能

Uno 是什麼?

這邊可以看看維基百科

Uno Command 是什麼?

簡言之,UnoCommand 是 LibreOffice 內部實作的功能,並且都有其對應的函數與參數。

詳細的 UnoCommand 可以參考官方文件看看有那些 UnoCommand 可以使用。

如何透過 C++ 來執行 UnoCommand 來擴充模組功能

 

LibreOfficeKit 提供開發者透過 postUnoCommand 直接使用 UNO Service,如下方所示的範例程式碼就是

 

//Using UnoCommand also can do the insertText
std::string json = R"MULTILINE(
        {
            "Text":
            {
                "type":"string",
                "value":"%s"
            }
        }
    )MULTILINE";
std::string args_str = Poco::format(json, msg);

lodoc->postUnoCommand(".uno:InsertText", args_str.c_str(), true);

 

Module 功能開發 --- python

擴充 Module 功能

模組功能開發的主要功能實作可以自由選擇以下但不限

  1. LibreOffice/OxOffice Extension
  2. uno command
  3. python script

此章節需要有 python 的先備知識,相信朋友們會點進來的都有 python 的基礎囉!

接下來我們會介紹 Python 在 LibreOffice 的架構與在模組的應用開發