Integrate Oxygen XML Web Author with a File Storage Service with a Custom Plugin
The bridge between Oxygen XML Web Author and File Storage Servers (e.g. GitHub, Drupal, Dropbox, etc.) is generally called a connector. This is basically a plugin whose purpose is to enable a connection to a file storage service.
Oxygen XML Web Author operates documents generically. It does not have built-in business
      logic for a specific File Storage Service. It has a few extension points, APIs, and interfaces
      that a connector must implement to make the actual connection to the file storage service.
      This generic approach is achieved by relying internally on java.net.URL
      objects.
When it needs to read a document, it opens an HTTP connection the document URL by using
        java.net.URL.openConnection(), and from the returned
        java.net.URLConnection, it reads the document from the
        java.io.InputStream returned by
        java.net.URLConnection.getInputStream() method call.
When it needs to write a document, it does the same as above but uses the
        java.io.OutputStream from java.net.URLConnection.
When it needs to list a directory, it checks that java.net.URLConnection
      implements the ro.sync.net.protocol.FileBrowsingConnection
      interface, and if it does, it calls ro.sync.net.protocol.FileBrowsingConnection.listFolder().
FileBrowsingConnection, the connector has to initialize the
        UrlChooser in the browser by executing the following code from a
        plugin.js
      file:workspace.setUrlChooser(new sync.api.FileBrowsingDialog());The connector has to contribute a ro.sync.ecss.extensions.api.webapp.plugin.URLStreamHandlerWithContext
      implementation that returns java.net.URLConnection objects that the Oxygen XML Web Author server can use. To do so, in the plugin.xml file, the
      connector has to define an extension of the type "URLHandler" with the class attribute
      pointing to the class that implements the ro.sync.exml.plugin.urlstreamhandler.URLStreamHandlerPluginExtension
      interface.
The URLStreamHandlerWithContext interface extends the standard
        java.net.URLStreamHandler interface, but it enables knowing what browser
      session to return a connection for.
  <extension type="URLHandler" 
    class="com.example.CustomUrlHandlerPluginExtension"/>com.example.CustomUrlHandlerPluginExtension may look like
      this:public class CustomUrlStreamHandlerPluginExtension implements URLStreamHandlerPluginExtension {
  @Override
  public URLStreamHandler getURLStreamHandler(String protocol) {
    if ("customprotocol".equals(protocol)) {
      return new CustomUrlStreamHandler(protocol);
    } else {
      return null;
    }
  }
}CustomUrlStreamHandler may look like
      this:public class CustomURLStreamHandlerWithContext extends URLStreamHandlerWithContext {
  @Override
  protected URLConnection openConnectionInContext(String contextId, URL oxyUri, Proxy proxy) throws IOException {
    return newCustomUrlConnection(contextId, oxyUri, proxy);
  }
}Note that the contextId argument of
        URLStreamHandlerWithContext.openConnectionInContext is actually the user
      session ID.
CustomUrlConnection may look like
      this:  public class CustomUrlConnection extends URLConnection implements FileBrowsingConnection {
    public CustomUrlConnection(String contextId, URL oxyUri, Proxy proxy) {
      super(oxyUri);
    }
    @Override
    public void connect() throws IOException {
      // TODO: implement me.
    }
    
    @Override
    public InputStream getInputStream() throws IOException {
      // TODO: implement me.
    }
    
    @Override
    public OutputStream getOutputStream() throws IOException {
      // TODO: implement me.
    }
    
    @Override
    public List<FolderEntryDescriptor> listFolder() throws IOException, UserActionRequiredException {
      // TODO: implement me.
    }
  }The CustomUrlStreamHandler from the above example will handle URLs with the
        "customprotocol" protocol (for example:
        "customprotocol://hostname/path/to/file/doc.xml"). This kind of URL is
      called an OXY-URL and it is used internally by the
      application. The user does not deal with this format. The URL of the Oxygen XML Web Author
      application that opens the above example URL would look like this:
        "http://localhost:8081/oxygen-xml-web-author/app/oxygen.html?url=customprotocol%3A%2F%2Fhostname%2Fpath%2Fto%2Ffile%2Fdoc.xml".
      Note that the document URL is specified in the "url" query parameter. You can see all the
      available query parameters in the Passing URL Parameters to the Editor topic. 
The CustomUrlConnection receives the OXY-URL and based on that information, it has to make a request to the File Storage
      Service to read a document, write a document, or list a directory.
URLConnection must signal this to Oxygen XML Web Author by throwing
          UserActionRequiredException with
          WebappMessage.MESSAGE_TYPE_CUSTOM like
      this:throw new UserActionRequiredException(
        new WebappMessage(
          WebappMessage.MESSAGE_TYPE_CUSTOM,
          "Authentication Required",
          "Please authenticate.",
          true));Authentication
The connector, when making requests to the File Storage Service, has to attach the required
        authentication information for a particular user (for a particular
          contextId). For example, the Authorization header that contains the
        user's credentials or a JWT. The connector typically stores the authentication information
        onto the session using the ro.sync.ecss.extensions.api.webapp.SessionStore API.
// Store authentication secret in the session store.
WebappPluginWorkspace pluginWorkspace = (WebappPluginWorkspace) pluginWorkspaceAccess;
SessionStore sessionStore = pluginWorkspace.getSessionStore();
sessionStore.put(sessionId, "custom-connector-auth", authenticationSecret);CustomUrlConnection would
        look:// Get authentication secret from the session store.
WebappPluginWorkspace pluginWorkspace = (WebappPluginWorkspace) pluginWorkspaceAccess;
SessionStore sessionStore = pluginWorkspace.getSessionStore();
String authenticationSecret = sessionStore.get(contextId, "custom-connector-auth");The connector may have to implement an OAuth flow, or to show username and password form, or
      even receive an authentication token from the user. In all cases, it has to store the
      authentication info into the SessionStore. For this, it can define a
        WebappServlet extension by extending the ro.sync.ecss.extensions.api.webapp.plugin.ServletPluginExtension API
      class. This allows the connector to register a servlet on a particular path (for e.g.
        "custom-connector-auth") where it can receive requests from the plugin's
      JavaScript code, allowing it to send data from the browser to the server. The servlet is
      supposed to receive the authentication info from the client, validate it, and then store it in
        SessionStore. The URL that the servlet handles may be like this:
          "http://localhost:8080/oxygen-xml-web-author/plugins-dispatcher/custom-connector-auth/".
      The servlet can obtain the session id like this:
      "req.getSession().getId()".
sync.api.FileServer implementation to
        sync.api.FileServersManager, somewhat like
      this:/**
 * See sync.api.FileServer.
 */
class CustomFileServer {
  /** @override */
  login(serverUrl, loginCallback) {
    // Show a dialog with username and password inputs and 
sends them to the connector servlet.
    let loginDialog = workspace.createDialog();
    loginDialog.setTitle("Login");
    loginDialog.setPreferredSize(400, 500);
    loginDialog.getElement().innerHTML = `
      <label for="name">Username:</label>
      <input type="text" id="name" name="name"><br><br>
      
      <label for="password">Password:</label>
      <input type="password" id="password" name="password"><br><br>
    `;
    loginDialog.onSelect(key => {
      if (key === 'ok') {
        let username = containerElement.querySelector("#name");
        let password = containerElement.querySelector("#password");
        // Sent the credentials to the connector servlet.
        fetch('../plugins-dispatcher/custom-connector-auth', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: `username=${encodeURIComponent(username.getValue())}&password=
${encodeURIComponent(password.getValue())}`
        })
          .then(response => {
            if (response.ok) {
              loginDialog.setVisible(false);
              logoutCallback();
            } else {
              workspace.getNotificationManager().showError("Authentication failed");
            }
          })
          .catch(() => {
            workspace.getNotificationManager().showError("An error occurred");
          });
      }
    })
    loginDialog.show();
  }
  /** @override */
  logout(logoutCallback) {
    //TODO Implement me.
  }
  /** @override */
  getUserName() {
    //TODO Implement me.
  }
  /** @override */
  setUserChangedCallback(userChangedCallback) {
    //TODO Implement me.
  }
  /** @override */
  createRootUrlComponent(rootUrl, rootURLChangedCallback, readOnly) {
    //TODO Implement me.
  }
  /** @override */
  getUrlInfo(url, urlInfoCallback, showErrorMessageCallback) {
    //TODO Implement me.
  }
}
/** @type {sync.api.FileServer} */
let customFileServer = new CustomFileServer();
/** @type {sync.api.FileServerDescriptor} */
let customFileServerDescriptor =  {
  'id' : 'custom-connector',
  'name' : 'Custom Connector',
  'icon' : null,
  'matches' : function matches(url) {
    return url.match(/^customprotocol?:/);
  },
  'fileServer' : customFileServer
};
workspace.getFileServersManager().registerFileServerConnector(customFileServerDescriptor);sync.api.FileServer.login method is called when authentication is
      requested. The implementation can show a dialog box that displays a username-password form and
      then submits the credentials to the connector servlet. To show a dialog box in the browser,
      the workspace.createDialog API can be used.The username is important to be returned by sync.api.FileServer.getUserName
      because otherwise, when adding comments or editing the document with change tracking, the user
      will appear as "Anonymous".
ServletPluginExtension that serves
        "/plugins-dispatcher/custom-connector-auth/" may look like
      this:public class CustomConnectorAuthenticationServlet extends ServletPluginExtension {
  
  @Override
  public void doPost(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws 
ServletException, IOException {
    String sessionId = httpRequest.getSession().getId();
    
    String user = httpRequest.getParameter("user");
    String passwd = httpRequest.getParameter("password");
    
    if (areCredentialsValid(user, passwd)) {
      WebappPluginWorkspace pluginWorkspace = (WebappPluginWorkspace) 
PluginWorkspaceProvider.getPluginWorkspace();
      SessionStore sessionStore = pluginWorkspace.getSessionStore();
      sessionStore.put(sessionId, "custom-connector-auth", user + ":" + passwd);
    } else {
      httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    }
  }
      
  private boolean areCredentialsValid(String user, String passwd) {
    // TODO Implement me.
    return false;
  }
  @Override
  public String getPath() {
    return "custom-connector-auth";
  }
}Troubleshooting
To troubleshoot the connector code, enable network logs by following the steps from the Enabling HTTP Request Logging for Debugging topic.
