Skip to main content

Customizing oXygen Author Component

In this article I'm going to guide you through the process of customization of oXygen Author Component. More specifically I'm going to create a new operation for the DITA framework that will generate and insert an xml fragment into the current document. While you can find the official documentation on this topic very useful, it misses any real code examples. Of course, you can download the author component startup project but it'll help those looking how to integrate the component as a Java Applet, thus, it's slightly irrelevant for us.

Requirements
In order to make a customization for the Author Component you need to have an oXygen standalone installation. For the development of Java customizations you'll need oxygen.jar on the classpath. This library is available in the oXygen installation directory as well as in the Author SDK project and in the author component startup project. Moreover, inside last two resources you can find javadocs and partial sources for oXygen classes. Unfortunately, these resources are not published in any Maven repository. So I've put them to the private corporate one to be able to create a pure Maven project. This is not a must though.

Customization
The customization process includes the following steps:
  1. First we should implement our own Java extension of the ro.sync.ecss.extensions.api.AuthorOperation interface which will show the custom dialog and then will use oXygen API to insert the proper code fragment at the caret position. The oXygen application is swing-based, so the custom dialog should extend javax.swing.JDialog. However, in our case it'd make more sense to reuse oXygen code and extend ro.sync.ecss.extensions.commons.ui.OKCancelDialog instead.
  2. Then pack the compiled classes into a JAR, say custom.jar, and put it inside the corresponding oXygen framework directory (or anywhere else of your choice).
  3. Then run the standalone oXygen installation, navigate to the Preferences->Document Type Associations page, edit the DITA document type and in the Classpath tab add a reference to the custom.jar. Then in the Author tab open the Actions subtab, create a new action based on your operation and then add it to the toolbar and menus in the corresponding subtabs. This configuration change will be saved in the OXYGEN_INSTALL_DIR\frameworks\dita\dita.framework file.
  4. As soon as a new operation works fine in the oXygen standalone installation, you can bundle the OXYGEN_INSTALL_DIR\frameworks\dita folder with the Author Component by wrapping it to obtain the frameworks.zip.jar archive. This last step is required if you want to distribute your change with a Java Applet.
Now let's look at the code snippet for the custom operation:
@API(type = APIType.INTERNAL, src = SourceType.PUBLIC)
public class InsertMyImageOperation extends InsertFragmentOperation {

    private static final String ARGUMENT_FRAGMENT = "fragment";

    public InsertMyImageOperation () {
        super();
    }

    /**
     * Loads the xml fragment and inserts it at the caret position.
     */
    @Override
    public void doOperation(AuthorAccess authorAccess, ArgumentsMap args) 
            throws AuthorOperationException {
        String xmlFragment = loadImageXmlFragment(authorAccess, args);
        super.doOperation(authorAccess, argsWithXmlFragment(args, xmlFragment));
    }

    /**
     * @see ro.sync.ecss.extensions.api.Extension#getDescription()
     */
    @Override
    public String getDescription() {
        return "Insert a custom image";
    }

    /**
     * Creates the custom dialog that returns the dialog info object.
     * The dialog info is used for xml fragment generation.
     */
    private String loadImageXmlFragment(AuthorAccess authorAccess, 
                                        ArgumentsMap args) {
        InsertMyImageDialog myImageDialog = new InsertMyImageDialog(
                (Frame) authorAccess.getWorkspaceAccess().getParentFrame(),
                authorAccess.getAuthorResourceBundle(), true);
        myImageDialog.setLocationRelativeTo(
                (Component) authorAccess.getWorkspaceAccess().getParentFrame());

        MyDialogInfo info = myImageDialog.showDialog();
        return MyImageFragmentGenerator.generateXml(info);
    }

    /**
     * Supplies the parent class arguments with the custom xml fragment.
     */
    private ArgumentsMap argsWithXmlFragment(final ArgumentsMap args, 
                                             final String xmlFragment) {
        return new ArgumentsMap() {
            @Override
            public Object getArgumentValue(String argumentName) {
                if (argumentName.equals(ARGUMENT_FRAGMENT)) {
                    return xmlFragment;
                } else {
                    return args.getArgumentValue(argumentName);
                }
            }
        };
    }
}
So the operation simply shows a custom dialog to the user, waits until it returns the required information, generates an xml fragment based on the user-supplied data and uses the oXygen API to insert the fragment properly.
I've reused a complex logic of ro.sync.ecss.extensions.commons.operations.InsertFragmentOperation that not only inserts the fragment for me but also provides some useful arguments that can be adjusted in the oXygen settings.
Here is the code snippet for the custom dialog:
@API(type = APIType.INTERNAL, src = SourceType.PUBLIC)
public class InsertMyImageDialog extends OKCancelDialog {

    public InsertMyImageDialog(Frame parentFrame, 
                               AuthorResourceBundle authorResourceBundle, 
                               boolean modal) {
        super(parentFrame, "My title", modal);
        JPanel mainPanel = new JPanel(new GridBagLayout());

        // Here custom components are being created and added to the mainPanel.

        add(mainPanel);
        configureSize();
    }

    /**
     * Shows the dialog and returns the information
     * that is required to generate the xml fragment.
     */
    public MyDialogInfo showDialog() {
        setVisible(true);
        return (getResult() == RESULT_OK) ? createMyDialogInfo() : null;
    }

    /**
     * Creates the information object with the dialog data.
     */
    private MyDialogInfo createMyDialogInfo() {
        MyDialogInfo info = new MyDialogInfo();

        // Here info object is being filled with the data from dialog components.

        return info;
    }

    /**
     * Sets proper dialog size.
     */
    private void configureSize() {
        setResizable(true);
        setMinimumSize(new Dimension(0, 0));
        pack();
        setMinimumSize(getSize());
    }
}
I've cleaned up the code to show the structure better. I've used two more custom classes above: MyDialogInfo and MyImageFragmentGenerator. Their meaning and logic must be clear from the code so I've left them out.

Comments

  1. Hi Ivan,

    Great blog post, your details are concise and exact.
    One more thing, for Oxygen 16 we are working to offer the Author Component project as a Maven archetype.

    Regards,
    Radu

    Radu Coravu
    XML Editor, Schema Editor and XSLT Editor/Debugger
    http://www.oxygenxml.com

    ReplyDelete
    Replies
    1. Thanks, Radu! Actually you helped a lot with your comments in emails while I was working on this. Maven support would be very useful indeed.

      Delete
  2. We also had a similar but not that detailed topic in the Oxygen users manual:

    http://www.oxygenxml.com/doc/ug-oxygen/#tasks/addCustomActionHowTo.html

    ReplyDelete

Post a Comment

Popular posts from this blog

Connection to Amazon Neptune endpoint from EKS during development

This small article will describe how to connect to Amazon Neptune database endpoint from your PC during development. Amazon Neptune is a fully managed graph database service from Amazon. Due to security reasons direct connections to Neptune are not allowed, so it's impossible to attach a public IP address or load balancer to that service. Instead access is restricted to the same VPC where Neptune is set up, so applications should be deployed in the same VPC to be able to access the database. That's a great idea for Production however it makes it very difficult to develop, debug and test applications locally. The instructions below will help you to create a tunnel towards Neptune endpoint considering you use Amazon EKS - a managed Kubernetes service from Amazon. As a side note, if you don't use EKS, the same idea of creating a tunnel can be implemented using a Bastion server . In Kubernetes we'll create a dedicated proxying pod. Prerequisites. Setting up a tunnel. ...

Notes on upgrade to JSF 2.1, Servlet 3.0, Spring 4.0, RichFaces 4.3

This article is devoted to an upgrade of a common JSF Spring application. Time flies and there is already Java EE 7 platform out and widely used. It's sometimes said that Spring framework has become legacy with appearance of Java EE 6. But it's out of scope of this post. Here I'm going to provide notes about the minimal changes that I found required for the upgrade of the application from JSF 1.2 to 2.1, from JSTL 1.1.2 to 1.2, from Servlet 2.4 to 3.0, from Spring 3.1.3 to 4.0.5, from RichFaces 3.3.3 to 4.3.7. It must be mentioned that the latest final RichFaces release 4.3.7 depends on JSF 2.1, JSTL 1.2 and Servlet 3.0.1 that dictated those versions. This post should not be considered as comprehensive but rather showing how I did the upgrade. See the links for more details. Jetty & Tomcat. JSTL. JSF & Facelets. Servlet. Spring framework. RichFaces. Jetty & Tomcat First, I upgraded the application to run with the latest servlet container versio...

Managing Content Security Policy (CSP) in IBM MAS Manage

This article explores a new system property introduced in IBM MAS 8.11.0 and Manage 8.7.0+ that enhances security but can inadvertently break Google Maps functionality within Manage. We'll delve into the root cause, provide a step-by-step solution, and offer best practices for managing Content Security Policy (CSP) effectively. Understanding the issue IBM MAS 8.11.0 and Manage 8.7.0 introduced the mxe.sec.header.Content_Security_Policy   property, implementing CSP to safeguard against injection attacks. While beneficial, its default configuration restricts external resources, causing Google Maps and fonts to malfunction. CSP dictates which domains can serve various content types (scripts, images, fonts) to a web page. The default value in this property blocks Google-related domains by default. Original value font-src 'self' data: https://1.www.s81c.com *.walkme.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' ...