PageSourceSearch

https://openrefine.org/assets/js/7ff7d6bc.1d57ddb5.js

js openrefine.org collected 2026-09-24 08:36:17 UTC 35,170 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkopenrefine_documentation=self.webpackChunkopenrefine_documentation||[]).push([[6694],{48737(e,n,t){t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"technical-reference/writing-extensions","title":"Writing extensions","description":"Introduction","source":"@site/docs/technical-reference/writing-extensions.md","sourceDirName":"technical-reference","slug":"/technical-reference/writing-extensions","permalink":"/docs/technical-reference/writing-extensions","draft":false,"unlisted":false,"editUrl":"https://github.com/OpenRefine/openrefine.github.com/edit/master/docs/technical-reference/writing-extensions.md","tags":[],"version":"current","lastUpdatedBy":"Rory Sawyer","lastUpdatedAt":1774458674000,"frontMatter":{"id":"writing-extensions","title":"Writing extensions","sidebar_label":"Extension technical reference"},"sidebar":"docs","previous":{"title":"Extension developer guidelines","permalink":"/docs/technical-reference/extension-dev-guidelines"},"next":{"title":"Migrating older extensions","permalink":"/docs/technical-reference/migrating-older-extensions"}}');var s=t(74848),r=t(28453);const o={id:"writing-extensions",title:"Writing extensions",sidebar_label:"Extension technical reference"},l=void 0,a={},c=[{value:"Introduction",id:"introduction",level:2},{value:"Directory Layout",id:"directory-layout",level:3},{value:"Sample extension",id:"sample-extension",level:2},{value:"Basic Structure",id:"basic-structure",level:3},{value:"Wiring Up the Extension",id:"wiring-up-the-extension",level:3},{value:"Extension points",id:"extension-points",level:2},{value:"Client-side: Javascript and CSS",id:"client-side-javascript-and-css",level:3},{value:"Client-side: HTML Templates",id:"client-side-html-templates",level:3},{value:"Client-side: Project UI Extension Points",id:"client-side-project-ui-extension-points",level:3},{value:"Main Menu",id:"main-menu",level:4},{value:"Column Header Menu",id:"column-header-menu",level:4},{value:"Cell renderers",id:"cell-renderers",level:4},{value:"Server-side: Ajax Commands",id:"server-side-ajax-commands",level:3},{value:"Server-side: Operations",id:"server-side-operations",level:3},{value:"Server-side: GREL",id:"server-side-grel",level:3},{value:"Server-side: Importers",id:"server-side-importers",level:3},{value:"Server-side: Exporters",id:"server-side-exporters",level:3},{value:"Server-side: Overlay Models",id:"server-side-overlay-models",level:3},{value:"Server-side: Scripting Languages",id:"server-side-scripting-languages",level:3},{value:"Testing",id:"testing",level:2},{value:"Java unit testing",id:"java-unit-testing",level:3},{value:"End to end testing in Cypress",id:"end-to-end-testing-in-cypress",level:3}
1,{value:"Testing OpenRefine with your extension installed",id:"testing-openrefine-with-your-extension-installed",level:4}];function d(e){const n={a:"a",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,s.jsx)(n.p,{children:"This is a very brief overview of the structure of OpenRefine extensions. For more detailed documentation and step-by-step guides please see the following external documentation/tutorials:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Giuliano Tortoreto has ",(0,s.jsx)(n.a,{href:"https://github.com/giTorto/OpenRefineExtensionDoc/raw/master/main.pdf",children:"written documentation detailling how to build extension for OpenRefine"})]}),"\n",(0,s.jsxs)(n.li,{children:["Owen Stephens has written ",(0,s.jsx)(n.a,{href:"http://www.meanboyfriend.com/overdue_ideas/2017/05/writing-an-extension-to-add-new-grel-functions-to-openrefine/",children:"a guide to developing an extension which adds new GREL functions to OpenRefine"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["OpenRefine makes use of a modified version of the ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/simile-butterfly/tree/openrefine",children:"Butterfly framework"}),' to provide an extension architecture. OpenRefine extensions are Butterfly modules. You don\'t really need to know about Butterfly itself, but you might encounter "butterfly" here and there in the code base.']}),"\n",(0,s.jsxs)(n.p,{children:["Extensions that come with the code base are located under ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/tree/master/extensions",children:"the extensions sub-directory"}),", but when you develop your own extension, you can put its code anywhere as long as you point Butterfly to it. That is done by any one of the following methods"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["refer to your extension's directory in ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/WEB-INF/butterfly.properties",children:"the butterfly.properties file"})," through a ",(0,s.jsx)(n.code,{children:"butterfly.modules.path"})," setting."]}),"\n",(0,s.jsxs)(n.li,{children:["specify the butterfly.modules.path property on the command line when you run OpenRefine. This overrides the values in the property file, so you need to include the default values first e.g. ",(0,s.jsx)(n.code,{children:"-Dbutterfly.modules.path=modules,../../extensions,/path/to/your/extension"})]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Please note that you should bundle any dependencies yourself, so you are insulated from OpenRefine packaging changes over time."}),"\n",(0,s.jsx)(n.h3,{id:"directory-layout",children:"Directory Layout"}),"\n",(0,s.jsx)(n.p,{children:"A OpenRefine extension sits in a file directory that contains the following files and sub-directories:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"pom.xml\n  src/\n      com/foo/bar/... *.java source files\n  module/\n      *.html, *.vt files\n      scripts/... *.js files\n      styles/... *.css files\n      images/... image files\n      MOD-INF/\n          lib/*.jar files\n          classes/... java class files\n          module.properties\n          controller.js\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The file named ",(0,s.jsx)(n.code,{children:"module.properties"})," (see ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/sample-extension/blob/master/module/MOD-INF/module.properties",children:"example"}),") contains the extension's metadata. Of importance is the name field, which gives the extension a name that's used in many other places to refer to it. This can be different from the extension's directory name."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"name = my-extension-name\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Your extension's client-side resources (.html, .js, .css files) stored in the ",(0,s.jsx)(n.code,{children:"module/"})," sub-directory will be accessible from ",(0,s.jsx)(n.a,{href:"http://127.0.0.1:3333/extension/my-extension-name/",children:"http://127.0.0.1:3333/extension/my-extension-name/"})," when OpenRefine is running."]}),"\n",(0,s.jsx)(n.p,{children:"Also of importance is the dependency"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"requires = core\n"})}),"\n",(0,s.jsx)(n.p,{children:"which makes sure that the core module of OpenRefine is loaded before the extension attempts to hook into it."}),"\n",(0,s.jsxs)(n.p,{children:["The file named ",(0,s.jsx)(n.code,{children:"controller.js"})," is responsible for registering the extension's hooks into OpenRefine. Look at the sample-extension extension's ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/sample-extension/blob/master/module/MOD-INF/controller.js",children:"controller.js"})," file for an example. It should have a function called ",(0,s.jsx)(n.code,{children:"init()"})," that does the hook registrations."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"pom.xml"})," file is an ",(0,s.jsx)(n.a,{href:"http://maven.apache.org/",children:"Apache Maven"})," build file. You can make a copy of the sample extension's ",(0,s.jsx)(n.code,{children:"pom.xml"})," file to get started. The important point here is that the Java code and dependencies should be output into the ",(0,s.jsx)(n.code,{children:"module/MOD-INF/lib"})," sub-directory."]}),"\n",(0,s.jsx)(n.p,{children:"Note that your extension's Java code would need to reference some libraries used in OpenRefine and OpenRefine's Java classes themselves. These dependencies are reflected in the Maven configuration for the extension."}),"\n",(0,s.jsx)(n.h2,{id:"sample-extension",children:"Sample extension"}),"\n",(0,s.jsxs)(n.p,{children:["You can copy the ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/sample-extension",children:"sample extension"})," and get started on writing your own extension. After you copy it, make sure you change its name inside its ",(0,s.jsx)(n.code,{children:"module/MOD-INF/controller.js"})," file."]}
1),"\n",(0,s.jsx)(n.h3,{id:"basic-structure",children:"Basic Structure"}),"\n",(0,s.jsxs)(n.p,{children:["In the sample extension, Java source code is contained under the ",(0,s.jsx)(n.code,{children:"src"})," sub-directory, and webapp code is under the ",(0,s.jsx)(n.code,{children:"module"})," sub-directory. Here is the full directory layout:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"sample-extension/\n      pom.xml (Maven configuration file)\n      src/\n          main/java/\n              com/google/refine/sampleExtension/\n                  ... Java source code ...\n          test/java/\n              com/google/refine/sampleExtension/\n                  ... Java test files ...\n      module/\n          MOD-INF/\n              module.properties (module settings)\n              controller.js (module init and routing logic in Javascript)\n          lib/\n              ... Java jars ...\n          ... velocity templates (.vt) ...\n          ... .css files ...\n          ... client-side files (.html, .css, .js, image files) ...\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The sub-directory ",(0,s.jsx)(n.code,{children:"MOD-INF"})," contains the Butterfly module's metadata and is what Butterfly looks for when it scans directories for modules. ",(0,s.jsx)(n.code,{children:"MOD-INF"})," serves similar functions as ",(0,s.jsx)(n.code,{children:"WEB-INF"})," in other web frameworks."]}),"\n",(0,s.jsxs)(n.p,{children:["Java code is built into the sub-directory ",(0,s.jsx)(n.code,{children:"classes"})," inside ",(0,s.jsx)(n.code,{children:"MOD-INF"}),", and supporting external Java jars are in the ",(0,s.jsx)(n.code,{children:"lib"})," sub-directory. Those will be automatically loaded by Butterfly. (The build.xml script is wired to compile into the ",(0,s.jsx)(n.code,{children:"classes"})," sub-directory.)"]}),"\n",(0,s.jsxs)(n.p,{children:["Client-side code is in the inner ",(0,s.jsx)(n.code,{children:"module"})," sub-directory. They can be plain old .html, .css, .js, and image files. There are also Velocity .vt files, but they need to be routed inside ",(0,s.jsx)(n.code,{children:"MOD-INF/controller.js"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"MOD-INF/controller.js"})," lets you configure the extension's initialization and URL routing in Javascript rather than in Java. For example, when the requested URL path is either ",(0,s.jsx)(n.code,{children:"/"})," or an empty string, we process and return ",(0,s.jsx)(n.code,{children:"MOD-INF/index.vt"})," ( ",(0,s.jsx)(n.a,{href:"http://127.0.0.1:3333/extension/sample/",children:"see http://127.0.0.1:3333/extension/sample/"})," if OpenRefine is running)."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"init()"})," function in ",(0,s.jsx)(n.code,{children:"controller.js"})," allows the extension to register various client-side handlers for augmenting pages served by Refine's core. These handlers are feature-specific. For example, ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/extensions/jython/module/MOD-INF/controller.js#L46",children:"this is where the jython extension adds its parser"}),". As for the sample extension, it adds its script ",(0,s.jsx)(n.code,{children:"project-injection.js"})," and style ",(0,s.jsx)(n.code,{children:"project-injection.css"})," into the ",(0,s.jsx)(n.code,{children:"/project"})," page. If you ",(0,s.jsx)(n.a,{href:"http://127.0.0.1:3333/project",children:"view the source of the /project page"}),", you will see references to those two files."]}),"\n",(0,s.jsx)(n.h3,{id:"wiring-up-the-extension",children:"Wiring Up the Extension"}),"\n",(0,s.jsxs)(n.p,{children:["The Extensions are loaded by the Butterfly framework. Butterfly refers to these as 'modules'. ",(0,s.jsxs)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/WEB-INF/butterfly.properties#L27",children:["The location of modules is set in the ",(0,s.jsx)(n.code,{children:"main/webapp/butterfly.properties"})," file"]}),". Butterfly simply descends into each of those paths and looks for any ",(0,s.jsx)(n.code,{children:"MOD-INF"})," directories."]}),"\n",(0,s.jsx)(n.h2,{id:"extension-points",children:"Extension points"}),"\n",(0,s.jsx)(n.h3,{id:"client-side-javascript-and-css",children:"Client-side: Javascript and CSS"}),"\n",(0,s.jsxs)(n.p,{children:["The UI in OpenRefine for working with a project is coded in ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/project.vt",children:"the /main/webapp/modules/core/project.vt file"}),". The file is quite small, and that's because almost all of its content is to be expanded dynamically through the Velocity variables ",(0,s.jsx)(n.code,{children:"$scriptInjection"})," and ",(0,s.jsx)(n.code,{children:"$styleInjection"}),". So that your own Javascript and CSS files get loaded, you need to register them with the ClientSideResourceManager, which is done in the /module/MOD-INF/controller.js file. See ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/sample-extension/blob/master/module/MOD-INF/controller.js",children:"the controller.js file in this sample extension code"})," for an example."]}),"\n",(0,s.jsxs)(n.p,{children:["In the registration call, the variable ",(0,s.jsx)(n.code,{children:"module"})," is already available to your code by default, and it refers to your own extension."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'ClientSideResourceManager.addPaths(\n        "project/scripts",\n        module,\n        [\n            "scripts/foo.js",\n            "scripts/subdir/bar.js"\n        ]\n    );\n'})}),"\n",(0,s.jsxs)(n.p,{children:["You can specify one or more files for registration, and their paths are relative to the ",(0,s.jsx)(n.code,{children:"module"})," sub-directory of your extension. They are included in the order listed."]}),"\n",(0,s.jsxs)(n.p,{children:["Javascript Bundling: Note that ",(0,s.jsx)(n.code,{children:"project.vt"})," belongs to the core module and is thus under the control of the core module's ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/MOD-INF/controller.js",children:"controller.js file"}),". The Javascript files to be included in ",(0,s.jsx)(n.code,{children:"project.vt"}
1)," are by default bundled together for performance. When debugging, you can prevent this bundling behavior by setting ",(0,s.jsx)(n.code,{children:"bundle"})," to ",(0,s.jsx)(n.code,{children:"false"})," near the top of that ",(0,s.jsx)(n.code,{children:"controller.js"})," file. (If you have commit access to this code base, be sure not to check that change in.)"]}),"\n",(0,s.jsx)(n.h3,{id:"client-side-html-templates",children:"Client-side: HTML Templates"}),"\n",(0,s.jsxs)(n.p,{children:["Beside Javascript, CSS, and images, your extension might also include HTML templates that get loaded on the fly by your Javascript code and injected into the page's DOM. For example, here is ",(0,s.jsx)(n.a,{href:"http://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/scripts/dialogs/clustering-dialog.html",children:"the Cluster edit dialog template"}),", which gets loaded by code in ",(0,s.jsx)(n.a,{href:"http://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/scripts/dialogs/clustering-dialog.js",children:"the equivalent javascript file 'clustering-dialog.js'"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'var dialog = $(DOM.loadHTML("core", "scripts/dialogs/clustering-dialog.html"));\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"DOM.loadHTML"})," returns the content of the file as a string, and ",(0,s.jsx)(n.code,{children:"$(...)"})," turns it into a DOM fragment. Where ",(0,s.jsx)(n.code,{children:'"core"'})," is, you would want your extension's name. The path of the HTML file is relative to your extension's ",(0,s.jsx)(n.code,{children:"module"})," sub-directory."]}),"\n",(0,s.jsx)(n.h3,{id:"client-side-project-ui-extension-points",children:"Client-side: Project UI Extension Points"}),"\n",(0,s.jsxs)(n.p,{children:["Getting your extension's Javascript code included in ",(0,s.jsx)(n.code,{children:"project.vt"})," doesn't accomplish much by itself unless your code also registers hooks into the UI. For example, you can surely implement an exporter in Javascript, but unless you add a corresponding menu command in the UI, your user can't use your exporter."]}),"\n",(0,s.jsx)(n.h4,{id:"main-menu",children:"Main Menu"}),"\n",(0,s.jsxs)(n.p,{children:["The main menu can be extended by calling any one of the methods ",(0,s.jsx)(n.code,{children:"MenuBar.appendTo"}),", ",(0,s.jsx)(n.code,{children:"MenuBar.insertBefore"}),", and ",(0,s.jsx)(n.code,{children:"MenuBar.insertAfter"}),". Each method takes 2 arguments: an array of strings that identify a particular existing menu item or submenu, and one new single menu item or submenu or an array of menu items and submenus. For example, to insert 2 menu items and a menu separator before the menu item Project > Export Filtered Rows > Templating..., write this Javascript code wherever that would execute when your Javascript files get loaded:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'MenuBar.insertBefore(\n        ["core/project", "core/export", "core/export-templating"],\n        [\n            {\n                "label":"Menu item 1",\n                "click": function() { ... }\n            },\n            {\n                "label":"Menu item 2",\n                "click": function() { ... }\n            },\n            {} // separator\n        ]\n    );\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The array ",(0,s.jsx)(n.code,{children:'["core/project", "core/export", "core/export-templating"]'})," pinpoints the reference menu item."]}),"\n",(0,s.jsxs)(n.p,{children:["See the beginning of ",(0,s.jsx)(n.a,{href:"http://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/scripts/project/menu-bar.js",children:"/main/webapp/modules/core/scripts/project/menu-bar.js"})," for IDs of menu items and submenus."]}),"\n",(0,s.jsx)(n.h4,{id:"column-header-menu",children:"Column Header Menu"}),"\n",(0,s.jsx)(n.p,{children:"The drop-down menu of each column can also be extended, but the mechanism is slightly different compared to the main menu. Because the drop-down menu for a particular column is constructed on the fly when the user actually clicks the drop-down menu button, extending the column header menu can't really be done once at start-up t
1ime, but must be done every time a column header menu gets created. So, registration in this case involves providing a function that gets called each such time:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) { ... do stuff to menu ... });\n"})}),"\n",(0,s.jsx)(n.p,{children:'That function takes in the column object (which contains the column\'s name), the column header UI object (generally not so useful), and the menu to extend. In the previous code line where it says "do stuff to menu", you can write something like this:'}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'MenuSystem.appendTo(menu, ["core/facet"], [\n        {\n            id: "core/text-facet",\n            label: "My Facet on " + column.name,\n            click: function() {\n                ... use column.name and do something ...\n            }\n        },\n    ]);\n'})}),"\n",(0,s.jsxs)(n.p,{children:["In addition to ",(0,s.jsx)(n.code,{children:"MenuSystem.a
1ppendTo"}),", you can also call ",(0,s.jsx)(n.code,{children:"MenuSystem.insertBefore"})," and ",(0,s.jsx)(n.code,{children:"MenuSystem.insertAfter"})," which the same 3 arguments. To see what IDs you can use, see the function ",(0,s.jsx)(n.code,{children:"DataTableColumnHeaderUI.prototype._createMenuForColumnHeader"})," in ",(0,s.jsx)(n.a,{href:"http://github.com/OpenRefine/OpenRefine/blob/master/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js",children:"/main/webapp/modules/core/scripts/views/data-table/column-header-ui.js"}),"."]}),"\n",(0,s.jsx)(n.h4,{id:"cell-renderers",children:"Cell renderers"}),"\n",(0,s.jsx)(n.p,{children:"From OpenRefine 3.7 onwards, extensions can also customize the way cells are rendered. This is done by registering a renderer, which is responsible for transforming the JSON representation of a cell into DOM elements rendering it:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"class MyCellRenderer {\n\n  constructor() {\n    super();\n    // some initialization code can be added here\n  }\n\n  render(\n    rowIndex, // the 0-based row index\n    cellIndex, // the position of the column to which the cell belongs\n    cell, // the deserialized JSON representation of the cell\n    cellUI // the parent CellUI object which called this renderer\n  ) {\n     // this renderer has the opportunity to return a DOM element represeting the cell, as follows:\n     return $('<span>rendered cell</span>');\n     // or it may not return anything, in which case the next cell renderer will be executed\n  }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["OpenRefine holds an ordered list of cell renderers in its ",(0,s.jsx)(n.code,{children:"CellRendererRegistry"}),". To render a cell, OpenRefine will execute each cell renderer in order, until the first renderer which returns a DOM element. The following renderers are not executed and that DOM element is used as the cell representation."]}),"\n",(0,s.jsx)(n.p,{children:"The registration of the renderer is done with"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-js",children:"CellRendererRegistry.addRenderer(\n    'my-renderer-identifier', // a string identifying our new renderer\n    new MyCellRenderer(), // the renderer itself\n    'recon' // the existing renderer it should be inserted before\n );\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The default renderers available in OpenRefine itself can be found in ",(0,s.jsx)(n.code,{children:"main/webapp/modules/core/scripts/views/data-table/cell-renderers/registry.js"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"server-side-ajax-commands",children:"Server-side: Ajax Commands"}),"\n",(0,s.jsxs)(n.p,{children:["The client-side of OpenRefine gets things done by calling AJAX commands on the server-side. These commands must be registered with the OpenRefine servlet, so that the servlet knows how to route AJAX calls from the client-side. This can be done inside the ",(0,s.jsx)(n.code,{children:"init"})," function in your extension's ",(0,s.jsx)(n.code,{children:"controller.js"})," file, e.g.,"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'function init() {\n      var RefineServlet = Packages.com.google.refine.RefineServlet;\n      RefineServlet.registerCommand(module, "my-command", new Packages.com.foo.bar.MyCommand());\n  }\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Your command will then be accessible at ",(0,s.jsx)(n.a,{href:"http://127.0.0.1:3333/command/my-extension/my-command",children:"http://127.0.0.1:3333/command/my-extension/my-command"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"server-side-operations",children:"Server-side: Operations"}),"\n",(0,s.jsxs)(n.p,{children:["Most commands change the project's data. Most of them do so by creating abstract operations. See the Changes, History, Processes, and Operations section of the ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/wiki/Server-Side-Architecture",children:"Server Side Architecture"})," document."]}),"\n",(0,s.jsxs)(n.p,{children:["You can register an operation ",(0,s.jsx)(n.strong,{children:"class"})," in the ",(0,s.jsx)(n.code,{children:"init"})," function as follows:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.operations.OperationRegistry.registerOperation(\n      module, \n      "operation-name",\n      Packages.com.foo.bar.MyOperation\n  );\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Do not call ",(0,s.jsx)(n.code,{children:"new"})," to construct an operation instance. You must register the class itself. The class should have a static function for reconstructing an operation instance from a JSON blob:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"static public AbstractOperation reconstruct(Project project, JSONObject obj) throws Exception {\n      ...\n  }\n"})}),"\n",(0,s.jsx)(n.h3,{id:"server-side-grel",children:"Server-side: GREL"}
1),"\n",(0,s.jsxs)(n.p,{children:["GREL can be extended with new functions. This is also done in the ",(0,s.jsx)(n.code,{children:"init"})," function in ",(0,s.jsx)(n.code,{children:"controller.js"}),", e.g.,"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.grel.ControlFunctionRegistry.registerFunction(\n        "functionName", new Packages.com.foo.bar.TheFunctionClass());\n'})}),"\n",(0,s.jsxs)(n.p,{children:["You might also want to provide new variables (beyond just ",(0,s.jsx)(n.code,{children:"value"}),", ",(0,s.jsx)(n.code,{children:"cells"}),", ",(0,s.jsx)(n.code,{children:"row"}),", etc.) available to expressions. This is done by registering a binder that implements the interface ",(0,s.jsx)(n.code,{children:"com.google.refine.expr.Binder"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"Packages.com.google.refine.expr.ExpressionUtils.registerBinder(\n        new Packages.com.foo.bar.MyBinder());\n"})}),"\n",(0,s.jsx)(n.h3,{id:"server-side-importers",children:"Server-side: Importers"}),"\n",(0,s.jsx)(n.p,{children:"You can register an importer as follows:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.importers.ImporterRegistry.registerImporter(\n      "importer-name", new Packages.com.foo.bar.MyImporter());\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The string ",(0,s.jsx)(n.code,{children:'"importer-name"'})," isn't important at all. It's not really related to file extension or mime-type. Just use something unique. Your importer will be explicitly called to test if it can import something."]}),"\n",(0,s.jsx)(n.h3,{id:"server-side-exporters",children:"Server-side: Exporters"}),"\n",(0,s.jsx)(n.p,{children:"You can register an exporter as follows:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.exporters.ExporterRegistry.registerExporter(\n      "exporter-name", new Packages.com.foo.bar.MyExporter());\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The string ",(0,s.jsx)(n.code,{children:'"exporter-name"'})," isn't important at all. It's only used by the client-side to tell the server-side which exporter to use. Just use something unique and, of course, relevant."]}),"\n",(0,s.jsx)(n.h3,{id:"server-side-overlay-models",children:"Server-side: Overlay Models"}),"\n",(0,s.jsxs)(n.p,{children:["Overlay models are objects attached onto a core Project object to store and manage additional data for that project. For example, the schema alignment skeleton is managed by the Protograph overlay model. An overlay model implements the interface ",(0,s.jsx)(n.code,{children:"com.google.refine.model.OverlayModel"})," and can be registered like so:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.model.Project.registerOverlayModel(\n      "model-name",\n      Packages.com.foo.bar.MyOverlayModel);\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Note that you register the ",(0,s.jsx)(n.strong,{children:"class"})," , not an instance. The class should implement the following static method for reconstructing an overlay model instance from a JSON blob:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"static public OverlayModel reconstruct(JSONObject o) throws JSONException {\n      ...\n  }\n"})}),"\n",(0,s.jsxs)(n.p,{children:["When the project gets saved, the overlay model instance's ",(0,s.jsx)(n.code,{children:"write"})," method will be called:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"public void write(JSONWriter writer, Properties options) throws JSONException {\n      ...\n  }\n"})}),"\n",(0,s.jsx)(n.h3,{id:"server-side-scripting-languages",children:"Server-side: Scripting Languages"}),"\n",(0,s.jsx)(n.p,{children:"A scripting language (such as Jython) can be registered as follows:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'Packages.com.google.refine.expr.MetaParser.registerLanguageParser(\n        "jython",\n        "Jython",\n        Packages.com.google.refine.jython.JythonEvaluable.createParser(),\n        "return value"\n    );\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The first string is the prefix that gets prepended to each expression so that we know 
1which language the expression is in. This should be short, unique, and identifying. The second string is a user-friendly name of the language. The third is an object that implements the interface ",(0,s.jsx)(n.code,{children:"com.google.refine.expr.LanguageSpecificParser"}),". The final string is the default expression in that language that would return the cell's value."]}),"\n",(0,s.jsx)(n.h2,{id:"testing",children:"Testing"}),"\n",(0,s.jsxs)(n.p,{children:["We recommend extension authors write backend Java unit tests, like OpenRefine core does, and, if applicable, frontend/integration tests can be written using Cypress. In addition to the resources below, extension authors are encouraged to review the documentation related to ",(0,s.jsx)(n.a,{href:"/docs/technical-reference/functional-tests",children:"functional tests"})," in OpenRefine core."]}),"\n",(0,s.jsx)(n.p,{children:"Please note that Java unit tests can be run from the command line with no additional setup, but integration tests need to have a running instance of OpenRefine to test against."}),"\n",(0,s.jsx)(n.h3,{id:"java-unit-testing",children:"Java unit testing"}),"\n",(0,s.jsxs)(n.p,{children:["The sample extension template includes a reference ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/sample-extension/blob/master/src/test/java/com/google/refine/sampleExtension/SampleUtilTest.java",children:"unit test suite"})," which leverages ",(0,s.jsxs)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/modules/core/src/test/java/com/google/refine/RefineTest.java",children:["the ",(0,s.jsx)(n.code,{children:"RefineTest"})," base class"]})," provided by OpenRefine core. Tests are not required to extend ",(0,s.jsx)(n.code,{children:"RefineTest"}),", but that class provides many utilities that may help you test your extension."]}),"\n",(0,s.jsx)(n.h3,{id:"end-to-end-testing-in-cypress",children:"End to end testing in Cypress"}),"\n",(0,s.jsxs)(n.p,{children:["OpenRefine uses ",(0,s.jsx)(n.a,{href:"https://www.cypress.io/",children:"Cypress"})," to automate frontend and integration testing. To get started with end-to-end testing, ",(0,s.jsx)(n.a,{href:"https://docs.cypress.io/app/end-to-end-testing/writing-your-first-end-to-end-test",children:"this guide in the Cypress docs"})," may be helpful. The ",(0,s.jsx)(n.a,{href:"/docs/technical-reference/functional-tests",children:"documentation for OpenRefine's functional tests"})," also provides guidance for writing Cypress tests. For reference, please see the ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/tree/master/main/tests/cypress",children:"Cypress tests in OpenRefine core"})," and the ",(0,s.jsx)(n.a,{href:"https://github.com/OpenRefine/CommonsExtension/tree/master/cypress",children:"Cypress tests in CommonsExtension"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["When running Cypress tests, it is important to remember that the instance of OpenRefine being tested must have the ",(0,s.jsx)(n.a,{href:"/docs/manual/installing#installing-extensions",children:"extension installed"}),". The ",(0,s.jsx)(n.code,{children:"REFINE_DATA_DIR"})," environment variable is a helpful way of configuring the location of your extensions."]}),"\n",(0,s.jsxs)(n.p,{children:["Cypress can also be run in headless mode, enabling end-to-end tests in CI/CD pipelines. The documentation for this feature can be found ",(0,s.jsx)(n.a,{href:"https://docs.cypress.io/app/continuous-integration/overview",children:"on the Cypress website"})," and an example script for running end-to-end tests can be found in ",(0,s.jsxs)(n.a,{href:"https://github.com/OpenRefine/OpenRefine/blob/master/refine#L364-L443",children:["the ",(0,s.jsx)(n.code,{children:"refine"})," shell script"]}),"."]}),"\n",(0,s.jsx)(n.h4,{id:"testing-openrefine-with-your-extension-installed",children:"Testing OpenRefine with your extension installed"}),"\n",(0,s.jsx)(n.p,{children:"Even without a dedicated test suite for the extension, Cypress tests can aid extension developers. The same setup above allows extension developers to run the core OpenRefine Cypress suite. This allows extension developers to confirm that their extension does not interfere with any core OpenRefine operations. Once again, install your extension in a dedicated test directory and start OpenRefine using that directory like so:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:'# note: your extension should be in a directory called "extensions" within this directory\nREFINE_DATA_DIR=/path/to/your/test/directory ./refine\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The Cypress tests can be run by following the remaining steps in the ",(0,s.jsx)(n.a,{href:"/docs/technical-reference/functional-tests",children:"functional tests documentation"}
1)," and will now run against an OpenRefine instance that has your extension installed."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453(e,n,t){t.d(n,{R:()=>o,x:()=>l});var i=t(96540);const s={},r=i.createContext(s);function o(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.