Difference between revisions of "AMI Custom Java Plugins"
Tag: visualeditor |
Tag: visualeditor |
||
Line 69: | Line 69: | ||
ami.db.auth.plugin.class=''fully_qualified_class_name'' | ami.db.auth.plugin.class=''fully_qualified_class_name'' | ||
== Example - Java Code == | == Example - Java Code == | ||
− | <syntaxhighlight lang="java"> | + | <syntaxhighlight lang="java" line="1"> |
package com.demo; | package com.demo; | ||
Line 576: | Line 576: | ||
== Example - Java Code == | == Example - Java Code == | ||
− | <syntaxhighlight lang="java"> | + | <syntaxhighlight lang="java" line="1"> |
package com.demo; | package com.demo; | ||
Line 676: | Line 676: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | == Example - Configuration == | ||
+ | ami.db.persister.plugins=com.demo.TestPersisterFactory | ||
+ | |||
+ | = Custom Java Objects in AmiScript (AMI One, AMI Center, AMI Web) = | ||
+ | |||
+ | == Overview == | ||
+ | AMI script is an object oriented language where objects can be declared and there methods executed. It is possible to write your own classes in java and make them accessible via AmiScript. | ||
+ | |||
+ | The class must have the com.f1.ami.web.AmiScriptAccessible annotation. Constructors and methods that should be accessible via AmiScript must also be annotated with AmiScriptAccessible. Note the annotation allows for overriding the ''name'' (and ''params'' for methods and constructors). | ||
+ | |||
+ | == Property name == | ||
+ | ami.db.persister.plugins=''comma_delimited_list_of_fully_qualified_java_class_names'' | ||
+ | |||
+ | == Example - Java Code == | ||
+ | <syntaxhighlight lang="java" line="1"> | ||
+ | package com.demo; | ||
+ | import com.f1.ami.web.AmiScriptAccessible; | ||
+ | |||
+ | @AmiScriptAccessible(name = "TestAccount") | ||
+ | public class TestClass { | ||
+ | private double price; | ||
+ | private int quantity; | ||
+ | private String name; | ||
+ | |||
+ | @AmiScriptAccessible | ||
+ | public TestClass(String name) { this.name = name; } | ||
+ | |||
+ | @AmiScriptAccessible(name = "setValue", params = { "px", "qty" }) | ||
+ | public void setValue(double price, int quantity) { | ||
+ | this.price = price; | ||
+ | this.quantity = quantity; | ||
+ | } | ||
+ | |||
+ | @AmiScriptAccessible(name = "describe") | ||
+ | public String describe() { | ||
+ | return quantity + "@" + price + " for " + name + " is " + (quantity * price); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == Example - Configuration == | ||
+ | amiscript.custom.classes=com.demo.TestClass | ||
+ | |||
+ | == Example - AmiScript == | ||
+ | TestAccount myAccount=new TestAccount("ABC"); | ||
+ | |||
+ | myAccount.setValue(40.5,1000); | ||
+ | |||
+ | session.log(myAccount.describe()); | ||
+ | |||
+ | = GUI Extensions (AMI One, AMI Web) = | ||
+ | |||
+ | == Overview == | ||
+ | AMI's frontend is broken up into "panels". Each panel has a particular type, ex: table, chart, heatmap, etc. You can implement your own panel types using the Web Panel plugin interface. | ||
+ | |||
+ | The Web Panel Plugin is a factory which generates AmiWebPluginPortlet (which represent an instance of a panel) | ||
+ | |||
+ | == Java interface (see javadoc for details) == | ||
+ | com.f1.ami.web.AmiWebPanelPlugin | ||
+ | |||
+ | == Property name == | ||
+ | ami.web.panels=''comma_delimited_list_of_fully_qualified_java_class_names'' | ||
+ | |||
+ | = Data Access Control Plugin = | ||
+ | |||
+ | == Overview == | ||
+ | This plugin control at a granular level what data a user can see. Here are the steps: | ||
+ | |||
+ | 1. A user is successfully logged in, determined by | ||
+ | |||
+ | com.f1.ami.web.auth.AmiAuthenticator plugin which can return as set of variables that are assigned to the user's session (these variables often come from some external corporate entitlements system). | ||
+ | |||
+ | 2. This user-session is passed into the | ||
+ | |||
+ | com.f1.ami.web.datafilter.AmiWebDataFilterPlugin which then returns a com.f1.ami.web.datafilter.AmiWebDataFilterinstance. Note that each user will generally have there "own" AmiWebDataFilter assigned to there session. | ||
+ | |||
+ | 3. As data is passed from the backend to the frontend its is first visited by the user's AmiWebDataFilter where the DataFilter can choose to suppress the data or not. There are two distinct ways data can be transferred from the "backend" to the user: | ||
+ | |||
+ | a. Realtime - As data is streamed into AMI, individual records are transported to the front end for display on a per-row basis. More specifically as rows are added*, updated** and deleted from the backend a corresponding message is sent to the frontend. | ||
+ | |||
+ | <nowiki>*</nowiki> See AmiWebDataFilter::evaluateNewRow(...) | ||
+ | |||
+ | <nowiki>**</nowiki> See AmiWebDataFilter::evaluateUpdateRow(...) | ||
+ | |||
+ | (Note, that deletes do not have a callback as it is not applicable for data filtering) | ||
+ | |||
+ | b. Query results - when the user invokes a query (generally via the EXECUTE command within a datamodel) a query object is constructed and sent back to the back end for execution*. Then, the backend responds with a table (or multiple tables) of data**. | ||
+ | |||
+ | <nowiki>*</nowiki>See AmiWebDataFilter::evaluateQueryRequest(...) | ||
+ | |||
+ | <nowiki>**</nowiki>See AmiWebDataFilter::evaluateQueryResponse(...) | ||
+ | |||
+ | == Java interface == | ||
+ | com.f1.ami.web.datafilter.AmiWebDataFilterPlugin | ||
+ | |||
+ | com.f1.ami.web.datafilter.AmiWebDataFilter | ||
+ | |||
+ | == Property name == | ||
+ | ami.web.data.filter.plugin.class=fully_qaulified_class_name | ||
+ | |||
+ | == Example - Configuration == | ||
+ | ami.web.data.filter.plugin.class=com.mysamples.SampleDataFilterPlugin | ||
+ | |||
+ | == Example - Java Code == | ||
+ | <syntaxhighlight lang="java" line="1"> | ||
+ | package com.mysamples; | ||
+ | |||
+ | import com.f1.ami.web.datafilter.AmiWebDataFilter; | ||
+ | import com.f1.ami.web.datafilter.AmiWebDataFilterPlugin; | ||
+ | import com.f1.ami.web.datafilter.AmiWebDataSession; | ||
+ | import com.f1.container.ContainerTools; | ||
+ | import com.f1.utils.PropertyController; | ||
+ | |||
+ | public class SampleDataFilterPlugin implements AmiWebDataFilterPlugin { | ||
+ | |||
+ | @Override | ||
+ | public void init(ContainerTools tools, PropertyController props) { | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public String getPluginId() { | ||
+ | return "DATAFILTER_PLUGIN"; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public AmiWebDataFilter createDataFilter(AmiWebDataSession session) { | ||
+ | return new SampleDataFilter(session); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | package com.mysamples; | ||
+ | |||
+ | import com.f1.ami.web.AmiWebObject; | ||
+ | import com.f1.ami.web.datafilter.AmiWebDataFilter; | ||
+ | import com.f1.ami.web.datafilter.AmiWebDataFilterQuery; | ||
+ | import com.f1.ami.web.datafilter.AmiWebDataSession; | ||
+ | import com.f1.base.Column; | ||
+ | import com.f1.base.Row; | ||
+ | import com.f1.utils.structs.table.columnar.ColumnarTable; | ||
+ | |||
+ | public class SampleDataFilter implements AmiWebDataFilter { | ||
+ | |||
+ | private AmiWebDataSession userSession; | ||
+ | private String allowedRegion; | ||
+ | |||
+ | public SampleDataFilter(AmiWebDataSession session) { | ||
+ | this.userSession = session; | ||
+ | allowedRegion = (String) userSession.getVariableValue("region"); | ||
+ | if(allowedRegion==null) | ||
+ | throw new RuntimeException("no 'region' specified"); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public byte evaluateNewRow(AmiWebObject realtimeRow) { | ||
+ | String region = (String) realtimeRow.getParam("region"); | ||
+ | return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public byte evaluateUpdatedRow(AmiWebObject realtimeRow, byte currentStatus) { | ||
+ | Object region = realtimeRow.getParam("region"); | ||
+ | return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public AmiWebDataFilterQuery evaluateQueryRequest(AmiWebDataFilterQuery query) { | ||
+ | return query; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public voidevaluateQueryResponse(AmiWebDataFilterQuery query, ColumnarTable table) { | ||
+ | Column regionColumn = table.getColumnsMap().get("region"); | ||
+ | if (regionColumn == null) | ||
+ | return; | ||
+ | for (int i = table.getSize() - 1; i >= 0; i--) { | ||
+ | Row row = table.getRow(i); | ||
+ | String region = row.getAt(regionColumn.getLocation(), String.class); | ||
+ | if (!allowedRegion.equals(region)) | ||
+ | table.removeRow(row); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | = AmiGuiService = | ||
+ | AmiGuiService - A mechanism for bridging AmiScript to/from JavaScript. This plugin enables JavaScript code to be accessible from AmiScript, through a well-defined AmiScript API. It's bidirectional: | ||
+ | |||
+ | * Calls to JavaScript from within the browser can, in turn, invoke AmiScript callbacks | ||
+ | * Calls to AmiScript from within the dashboard can, in turn, call JavaScript within the browser. | ||
+ | |||
+ | == How this works == | ||
+ | In order to interact with AmiScript in the webserver and with JavaScript in the web browser, two blocks of adapter code must be written. Each of these two blocks of code are started as singletons which communicate over the AMI HTTP[S] transport (this transport is, however, transparent to you when building these adapters). The webserver's singleton is one per user session and the JavaScript singleton object is started when the browser loads. These two objects are responsible for communication between their respective environments and each other: | ||
+ | |||
+ | |||
+ | [[File:AmiGuiService.jpg|frameless|360x360px]] | ||
+ | |||
+ | == Deep Dive == | ||
+ | |||
+ | === '''Initializing''' === | ||
+ | |||
+ | # First, a Java plugin implementing the ''com.f1.ami.web.guiplugin.AmiWebGuiServicePlugin'' is initiated when the Ami Web Server starts up. The class name must be specified in the ami.guiservice.plugins property. Note, that only one instance is started up per JVM. | ||
+ | # Each time a user logs in, the ''AmiWebGuiServicePlugin::createGuiIntegrationAdapter'' is called which returns a custom class implementing the AmiWebGuiServiceAdapter interface. | ||
+ | # Each time the page is refreshed, including on initial login, the following methods are called on the AmiWebGuiServiceAdapter (These methods initialize the browser's JavaScript environment): | ||
+ | |||
+ | |||
+ | # ''getJavascriptLibraries''() -> Your adapter returns a list of libraries to load. This tells the browser which libraries to load, if any, for this custom adapter. | ||
+ | # ''getJavascriptInitialization''() -> Your adapter should return JavaScript to be executed in the browser at pageLoad and allows for any custom initialization. | ||
+ | # ''getJavascriptNewInstance''() -> Your adapter must return the JavaScript necessary for generating a JavaScript singleton that will receive/send messages. This JavaScript must implement a function called ''registerAmiGuiServicePeer(peer)'' which, typically, just stores the peer argument in a member variable for later use. |
Revision as of 04:59, 4 March 2021
Overview
AMI is an extendable platform such that java plugins can be integrated at various touch points throughout the product. There are various types of plugins, each for supporting a particular purpose. Regardless of type, there are certain guidelines to follow when embedding a plugin into AMI:
1. Write a Java class which implements the appropriate interface.
- The various interfaces are discussed in the following sections.
- Each plugin should have a universally unique ID, returned by getPluginId()
- Many plugins operate as "factories" which create instances of class. For example, the Datasource Plugin creates Datasource Adapters on demand
- The compiled class(es) must be added to the classpath. This is most easily done by bundling them into a jar and placing the jar in the lib directory. All jars in the lib directory are automatically added to the class path
2. Add the fully qualified Java class name to the appropriate property.
- The name of the property coincides with the type of plugin
- Defaults for a given plugin type are found in the config/defaults.properties file. You should override the property in the config/local.properties file).
In all cases, the plugins indirectly implement the AmiPlugin interface. Plugins are instantiated and initialized during startup, such that the failure of a plugin to startup will cause AMI to hard fail on startup. The exact reason for failure can be found in the log files.
Interfacing with Directory Naming Service (AMI One, Center)
Overview
In Enterprise environments, some services cannot be directly identified by a physical destination (ex: host name) and are instead logically identified. In this situation, the organization implements a directory naming service that can map, in realtime, the logical identifier to a physical destination. For AMI to access resources in this scenario, a plugin must be written that interfaces with the directory naming service. Then, when a resource is requested inside AMI, AMI will first ask the Plugin to "resolve" the logical name to a physical one, passing the resolved physical one to the underlying connectors. It's the plugin's responsibility to connect to the naming service and provide an answer in a timely fashion.
Using Multiple Resolvers
Note, that many resolvers can be supplied. The order in which they are defined in the property is the order in which they are visited. Once a resolver plugin says it "canResolve" the identifier, the remaining resolvers are not called.
Default case
If no resolvers plugins are provided, or none of the resolvers "canResolve(...)" a given identifier, then the identifier is considered a physical identifier and passed straight to the connector.
Java interface (see javadoc for details)
com.f1.ami.amicommon.AmiNamingServiceResolver
Property name
ami.naming.service.resolvers=comma_delimited_list_of_fully_qualified_java_class_names
Interfacing with Single Sign on and Entitlements (AMI One, Center, Web)
Overview
When a user attempts to access AMI, first it's necessary to validate the user should be granted access, through a valid user name and password. If the user should be granted, then certain attributes may need to be associated with the user that AMI can use to dictate fine-grained access.
There are two different entry points into AMI, each of which can have their own instance of an authentication adapter:
- Frontend Web Interface - When accessing AMI through a browser, first the user must supply a user name and password via the html login page (see property name for front end web access)
- Backend Command line interface - When accessing AMI's in-memory database using the command line interface, first the user must execute the login command, which in turn calls an instance of this plugin (see property name for backend command line access)
AMI Predefined Attributes
Attribute | Description |
---|---|
ISADMIN | If true, the user will be logged into the website with admin rights |
ISDEV | If true, the user will be logged into the website with developer rights |
DEFAULT_LAYOUT | If set, this will be the default layout loaded on login |
LAYOUTS | A comma delimited list of regular expressions for layouts that are available |
amivar_some_varname | A variable named user.some_varname of type string is added to the user's session. This has been deprecated, use amiscript.variable |
amiscript.variable.some_varname | A variable named varname of the supplied type is added to the user's session |
AMIDB_PERMISSIONS | A comma delimited combination of READ,WRITE,ALTER and EXECUTE which controls permissions for the user when logging in via jdbc or db command line |
Java interface (see javadoc for details)
com.f1.ami.web.auth.AmiAuthenticator
Property name for front end web access
ami.auth.plugin.class=fully_qualified_class_name
Property name for backend command line access
ami.db.auth.plugin.class=fully_qualified_class_name
Example - Java Code
1package com.demo;
2
3import java.util.ArrayList;
4import java.util.List;
5
6import com.f1.container.ContainerTools;
7import com.f1.utils.PropertyController;
8
9public class TestAuthenticator implements AmiAuthenticator {
10
11 @Override
12 public void init(ContainerTools tools, PropertyController props) {
13 // TODO Auto-generated method stub
14 }
15
16 @Override
17 public AmiAuthResponse authenticate(String namespace, String location, String user, String password) {
18 final List<AmiAuthAttribute> attributes = new ArrayList<AmiAuthAttribute>();
19 attributes.add(new BasicAmiAttribute("ISDEV", "true"));
20 attributes.add(new BasicAmiAttribute("ISADMIN", "true"));
21 attributes.add(new BasicAmiAttribute("ami_layout_shared", "default_layout.ami"));
22 return new BasicAmiAuthResponse(AmiAuthResponse.STATUS_OKAY, null, new BasicAmiAuthUser(user, "Jackie", "Davenson", "777-888-9999", "jDavenson@mail.com", "Tire Co.", attributes));
23 }
24
25 @Override
26 public String getPluginId() {
27 return "TestAuthenticator";
28 }
29}
Example - Configuration
ami.auth.plugin.class=com.demo.TestAuthenticatorPlugin
Connections to Custom External Datasources (AMI One, Center)
Overview
Connecting to external datasources or systems for accessing and uploading data is at the core of what AMI does. There are dozens of adapters out of the box for well known databases, file formats, etc. Large organizations that have custom databases/storage systems can access them in AMI by implementing a datasource plugin.
Each datasource can optionally support the following functionality:
- Providing a list of available tables
- Providing for a sample of data for a given table
- Running a query (Downloading data into AMI)
- Uploading data from AMI into the datasource
Java interface (see javadoc for details)
com.f1.ami.amicommon.AmiDatasourcePlugin
com.f1.ami.amicommon.AmiDatasourceAdapter
Property name for front end web access
ami.datasource.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example Java Code
1package com.demo;
2
3import java.util.HashMap;
4import java.util.Map;
5
6import com.f1.ami.amicommon.AmiDatasourceAdapter;
7import com.f1.ami.amicommon.AmiDatasourcePlugin;
8import com.f1.container.ContainerTools;
9import com.f1.utils.PropertyController;
10
11public class TestDatasourcePlugin implements AmiDatasourcePlugin {
12 private static final Map<String, Object> OPERATORS_MAP = new HashMap<String, Object>();
13 private static final Map<String, Object> WHERE_SYNTAX_MAP = new HashMap<String, Object>();
14 private static final Map<String, Object> HELP_MAP = new HashMap<String, Object>();
15 static {
16 OPERATORS_MAP.put("eq", "=");
17 OPERATORS_MAP.put("ne", "!=");
18 OPERATORS_MAP.put("lt", "<");
19 OPERATORS_MAP.put("gte", ">=");
20 WHERE_SYNTAX_MAP.put("prefix", "((");
21 WHERE_SYNTAX_MAP.put("suffix", "))");
22 WHERE_SYNTAX_MAP.put("join", ") or (");
23 WHERE_SYNTAX_MAP.put("true", "true");
24 }
25
26 @Override
27 public void init(ContainerTools tools, PropertyController props) {
28 }
29
30 @Override
31 public String getPluginId() {
32 return "TestDatasource";
33 }
34
35 @Override
36 public String getDatasourceDescription() {
37 return "Test";
38 }
39
40 @Override
41 public AmiDatasourceAdapter createDatasourceAdapter() {
42 return new TestDatasourceAdapter();
43 }
44
45 @Override
46 public String getDatasourceIcon() {
47 return "../../../../resources/test.PNG";
48 }
49
50 @Override
51 public String getDatasourceQuoteType() {
52 return "\"";
53 }
54
55 @Override
56 public Map<String, Object> getDatasourceOperators() {
57 return OPERATORS_MAP;
58 }
59
60 @Override
61 public Map<String, Object> getDatasourceWhereClauseSyntax() {
62 return WHERE_SYNTAX_MAP;
63 }
64
65 @Override
66 public Map<String, Object> getDatasourceHelp() {
67 return HELP_MAP;
68 }
69}
70
71package com.demo;
72
73import java.util.ArrayList;
74import java.util.List;
75
76import com.f1.ami.amicommon.AmiDatasourceAdapter;
77import com.f1.ami.amicommon.AmiDatasourceException;
78import com.f1.ami.amicommon.AmiDatasourceTracker;
79import com.f1.ami.amicommon.AmiServiceLocator;
80import com.f1.ami.amicommon.msg.AmiCenterQuery;
81import com.f1.ami.amicommon.msg.AmiCenterQueryResult;
82import com.f1.ami.amicommon.msg.AmiCenterUpload;
83import com.f1.ami.amicommon.msg.AmiDatasourceTable;
84import com.f1.base.Columns;
85import com.f1.base.Row;
86import com.f1.container.ContainerTools;
87import com.f1.utils.structs.table.BasicTable;
88
89public class TestDatasourceAdapter implements AmiDatasourceAdapter {
90 private ContainerTools tools;
91 private AmiServiceLocator serviceLocator;
92
93 @Override
94 public void init(ContainerTools tools, AmiServiceLocator serviceLocator) throwsAmiDatasourceException {
95 this.tools = tools;
96 this.serviceLocator = serviceLocator;
97 }
98
99 @Override
100 publicList<AmiDatasourceTable> getTables(AmiDatasourceTracker debugSink) throwsAmiDatasourceException {
101 List<AmiDatasourceTable> tables = newArrayList<AmiDatasourceTable>();
102 AmiDatasourceTable table = tools.nw(AmiDatasourceTable.class);
103 table.setCollectionName("master");
104 table.setName("accounts");
105 table.setCustomQuery("SELECT * FROM accounts WHERE ${WHERE}");
106 tables.add(table);
107 return tables;
108 }
109
110 @Override
111 publicList<AmiDatasourceTable> getPreviewData(List<AmiDatasourceTable> tables, int previewCount, AmiDatasourceTracker debugSink) throwsAmiDatasourceException {
112 for (int i = 0; i < tables.size(); i++) {
113 AmiDatasourceTable table = tables.get(i);
114 AmiCenterQuery q = tools.nw(AmiCenterQuery.class);
115 q.setQuery(table.getCustomQuery());
116 q.setLimit(previewCount);
117 AmiCenterQueryResult rs = tools.nw(AmiCenterQueryResult.class);
118 processQuery(q, rs, debugSink);
119 List<Columns> results = rs.getTables();
120 if (results.size() > 0)
121 table.setPreviewData(results.get(i));
122 }
123 return tables;
124 }
125
126 @Override
127 public AmiServiceLocator getServiceLocator() {
128 return serviceLocator;
129 }
130
131 @Override
132 public void processQuery(AmiCenterQuery query, AmiCenterQueryResult resultSink, AmiDatasourceTracker debugSink) throws AmiDatasourceException {
133 String queryStatement = query.getQuery();
134 // Do something with query statement
135 List<Columns> result = newArrayList<Columns>();
136 BasicTable table = new BasicTable();
137 String id = "id";
138 String reputation = "reputation";
139 String isPaid = "isPaid";
140 table.addColumn(String.class, id);
141 table.addColumn(Integer.class, reputation);
142 bt.addColumn(Boolean.class, isPaid);
143 Row row = bt.newRow("superman123", 150, true);
144 bt.getRows().add(row);
145 Row row2 = bt.newRow("trucker66", 400, true);
146 bt.getRows().add(row2);
147 resultSink.setTables(result);
148 }
149
150 @Override
151 public boolean cancelQuery() {
152 return false;
153 }
154
155 @Override
156 public void processUpload(AmiCenterUpload upload, AmiCenterQueryResult resultsSink, AmiDatasourceTracker tracker) throwsAmiDatasourceException {
157 throw newAmiDatasourceException(AmiDatasourceException.UNSUPPORTED_OPERATION_ERROR, "Upload to datasource");
158 }
159}
Example - Configuration
ami.datasource.plugins=com.demo.TestDatasourcePlugin
Custom Triggers (AMI One, AMI Center)
Overview
AMI's in-memory database is a comprehensive and realtime SQL storage engine that can be extended using Java Plugins. The trigger plugin is a factory used to create triggers as defined in the imdb schema.
Example
Consider the Ami Script example:
CREATE TRIGGER mytrigger OFTYPE MyRiskCalc ON myTable USE myoption="some_value"
The above sample command will cause AMI to:
- Look for a registered AmiTriggerFactory with the id "MyRiskCalc".
- Call newTrigger() on the factory.
- Call startup(...) on the returned, newly generated trigger. Note that the startup will contain the necessary bindings:
- Trigger name (ex: mytrigger)
- Options (ex: myoption=some_value)
- Target tables (ex: myTable)
Java interface (see javadoc for details)
com.f1.ami.center.triggers.AmiTriggerFactory
Property name
ami.db.trigger.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example – Java Code
1package com.demo;
2
3import java.util.ArrayList;
4import java.util.Collection;
5import java.util.List;
6
7import com.f1.ami.amicommon.AmiFactoryOption;
8import com.f1.ami.center.triggers.AmiTrigger;
9import com.f1.ami.center.triggers.AmiTriggerFactory;
10import com.f1.anvil.triggers.AnvilTriggerOrdersBySymSide;
11import com.f1.container.ContainerTools;
12import com.f1.utils.PropertyController;
13
14public class TestTriggerFactory implements AmiTriggerFactory {
15 private List<AmiFactoryOption> options = new ArrayList<AmiFactoryOption>();
16
17 @Override
18 public Collection<AmiFactoryOption> getAllowedOptions() {
19 return options;
20 }
21
22 @Override
23 public void init(ContainerTools tools, PropertyController props) {
24 }
25
26 @Override
27 public String getPluginId() {
28 return "TESTTRIGGER";
29 }
30
31 @Override
32 public AmiTrigger newTrigger() {
33 return new TestTrigger();
34 }
35}
36
37package com.demo;
38
39import com.f1.ami.center.table.AmiPreparedQuery;
40import com.f1.ami.center.table.AmiPreparedQueryCompareClause;
41import com.f1.ami.center.table.AmiPreparedRow;
42import com.f1.ami.center.table.AmiRow;
43import com.f1.ami.center.table.AmiTable;
44import com.f1.ami.center.triggers.AmiAbstractTrigger;
45
46public class TestTrigger extends AmiAbstractTrigger {
47
48 @Override
49 public void onStartup() {
50 // TOD Auto-generated method stub
51 }
52
53 @Override
54 public void onInserted(AmiTable table, AmiRow row) {
55 // TODO Auto-generated method stub
56 }
57
58 @Override
59 public boolean onInserting(AmiTable table, AmiRow row) {
60 // TODO Auto-generated method stub
61 return super.onInserting(table, row);
62 }
63
64 @Override
65 public void onUpdated(AmiTable table, AmiRow row) {
66 // TODO Auto-generated method stub
67 }
68
69 @Override
70 public boolean onUpdating(AmiTable table, AmiRow row) {
71 // TODO Auto-generated method stub
72 return super.onUpdating(table, row);
73 }
74
75 @Override
76 public boolean onDeleting(AmiTable table, AmiRow row) {
77 // TODO Auto-generated method stub
78 return super.onDeleting(table, row);
79 }
80}
Example - Configuration
ami.db.trigger.plugins=com.demo.TestTriggerFactory
Custom Stored Procedure (AMI One, AMI Center)
Overview
AMI's in-memory database is a comprehensive and realtime SQL storage engine that can be extended using Java Plugins. The stored procedure plugin is a factory used to create stored procedures as defined in the imdb schema.
Example
Consider the Ami Script example:
CREATE PROCEDURE myproc OFTYPE MyCustProc USE myoption="some_value"
The above sample command will cause AMI to:
- Look for a registered AmiStoredProcFactory with the id "MyCustProc".
- Call newStoredProc() on the factory.
- Call startup(...) on the returned, newly generated storedproc. Note that the startup will contain the necessary bindings:
- Procedure name (ex: myproc)
- Options (ex: myoption=some_value)
- Target tables (ex: myTable)
Java interface (see javadoc for details)
com.f1.ami.center.procs.AmiStoredProcFactory
Property name
ami.db.procedure.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example - Configuration
ami.db.procedure.plugins=com.demo.TestProcFactory
Custom Timer (AMI One, AMI Center)
Overview
AMI's in-memory database is a comprehensive and realtime SQL storage engine that can be extended using Java Plugins. The timer plugin is a factory used to create timers as defined in the imdb schema.
Example
Consider the Ami Script example:
CREATE TIMER mytimer OFTYPE MyStartupTimer ON "0 0 0 0 MON-FRI UTC " USE myoption="some_value"
The above sample command will cause AMI to:
- Look for a registered AmiStoredProcFactory with the id "MyStartupTimer".
- Call newTimer() on the factory.
- Call startup(...) on the returned, newly generated storedproc. Note that the startup will contain the necessary bindings:
- Timer name (ex: mytimer)
- Options (ex: myoption=some_value)
- Schedule, Priority, etc.
Java interface (see javadoc for details)
com.f1.ami.center.timers.AmiTimerFactory
Property name
ami.db.timer.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example - Java Code
1package com.demo;
2
3import java.util.ArrayList;
4import java.util.Collection;
5import java.util.List;
6
7import com.f1.ami.amicommon.AmiFactoryOption;
8import com.f1.container.ContainerTools;
9import com.f1.utils.PropertyController;
10
11public class TestTimerFactory implements AmiTimerFactory {
12
13 private List<AmiFactoryOption> options = new ArrayList<AmiFactoryOption>();
14
15 public TestTimerFactory() {
16 // TODOAuto-generated method stub
17 }
18
19 @Override
20 public void init(ContainerTools tools, PropertyController props) {
21 // TODOAuto-generated method stub
22 }
23
24 @Override
25 public Collection<AmiFactoryOption> getAllowedOptions() {
26 return options;
27 }
28
29 @Override
30 public AmiTimer newTimer() {
31 return new TestTimer();
32 }
33
34 @Override
35 public String getPluginId() {
36 return "TESTTIMER";
37 }
38}
39
40package com.demo;
41
42import com.f1.ami.center.table.AmiImdb;
43
44public class TestTimer extends AmiAbstractTimer {
45
46 @Override
47 public void onTimer(long scheduledTime) {
48 // TODOAuto-generated method stub
49 }
50
51 @Override
52 public void onSchemaChanged(AmiImdb imdb) {
53 // TODOAuto-generated method stub
54 }
55
56 @Override
57 protected void onStartup() {
58 // TODOAuto-generated method stub
59 }
60}
Example - Configuration
ami.db.timer.plugins=com.demo.TestTimerFactory
================
TIMER PROCEDURES
================
__SCHEDULE_TIMER (TimerName String nonull, Delay long nonull)
runs a scheduled execution of timer with TimerName, Delay is in milliseconds from now
Example:
CALL __SCHEDULE_TIMER("t1",15000);
__SHOW_TIMER_ERROR (TimerName String nonull)
shows last error of timer with TimerName
Example:
CALL __SCHEDULE_TIMER_ERROR("t2");
__RESET_TIMER_STATS (TimerName String nonull,ExecutedStats boolean nonull, ErrorStats boolean nonull)
resets ExecutedCount and/or ErrorCount of timer with TimerName
Example:
CALL __RESET_TIMER_STATS("t1", true, false);
Custom Persistence Factory (AMI One, AMI Center)
Overview
AMI's in-memory database is a comprehensive and realtime SQL storage engine that can be extended using Java Plugins. The persistence plugin is a factory used to create table persister instances as defined in the imdb schema.
Example
Consider the Ami Script example:
CREATE TABLE mytable(col1 int) USE PersistEngine="MyPersister" PersistOptions="myoption=some_val"
The above sample command will cause AMI to:
- Create a table (named "mytable") with the specified columns (col1)
- Look for a registered AmiTablePersisterFactory with the id "MyPersister".
- Call newPersister() on the factory, passing in a map of supplied options (myoption=some_val)
- Call init(...) on the returned, newly generated persister.
Java interface (see javadoc for details)
com.f1.ami.center.table.persist.AmiTablePersisterFactory
Property name
ami.db.persister.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example - Java Code
1package com.demo;
2
3import java.util.Collection;
4import java.util.Collections;
5import java.util.Map;
6
7import com.f1.ami.amicommon.AmiFactoryOption;
8import com.f1.container.ContainerTools;
9import com.f1.utils.PropertyController;
10
11public class TestPersisterFactory implements AmiTablePersisterFactory {
12
13 @Override
14 public void init(ContainerTools tools, PropertyController props) {
15 // TODOAuto-generated method stub
16 }
17
18 @Override
19 public AmiTablePersister newPersister(Map<String, Object> options) {
20 return new TestPersister();
21 }
22
23 @Override
24 public String getPluginId() {
25 return "TESTPERSISTER";
26 }
27
28 @Override
29 public Collection<AmiFactoryOption> getAllowedOptions() {
30 return Collections.EMPTY_LIST;
31 }
32
33}
34
35package com.demo;
36
37import java.io.IOException;
38
39import com.f1.ami.center.AmiSysCommandsUtils;
40import com.f1.ami.center.table.AmiImdbImpl;
41import com.f1.ami.center.table.AmiRowImpl;
42import com.f1.ami.center.table.AmiTable;
43import com.f1.utils.LH;
44
45public class TestPersister implements AmiTablePersister {
46
47 @Override
48 public void init(AmiTable sink) {
49 // TODOAuto-generated method stub
50 }
51
52 @Override
53 public void onRemoveRow(AmiRowImpl row) {
54 // TODOAuto-generated method stub
55 }
56
57 @Override
58 public void onAddRow(AmiRowImpl r) {
59 // TODOAuto-generated method stub
60 }
61
62 @Override
63 public void onRowUpdated(AmiRowImpl sink, long updatedColumns) {
64 // TODOAuto-generated method stub
65 }
66
67 @Override
68 public void loadTableFromPersist() {
69 // TODOAuto-generated method stub
70 }
71
72 @Override
73 public void saveTableToPersist() {
74 // TODOAuto-generated method stub
75 }
76
77 @Override
78 public void clear() {
79 // TODOAuto-generated method stub
80 }
81
82 @Override
83 public void flushChanges() {
84 // TODOAuto-generated method stub
85 }
86
87 @Override
88 public void drop() {
89 // TODOAuto-generated method stub
90 }
91
92 @Override
93 public void onTableRename(String oldName, String name) {
94 // TODOAuto-generated method stub
95 }
96}
Example - Configuration
ami.db.persister.plugins=com.demo.TestPersisterFactory
Custom Java Objects in AmiScript (AMI One, AMI Center, AMI Web)
Overview
AMI script is an object oriented language where objects can be declared and there methods executed. It is possible to write your own classes in java and make them accessible via AmiScript.
The class must have the com.f1.ami.web.AmiScriptAccessible annotation. Constructors and methods that should be accessible via AmiScript must also be annotated with AmiScriptAccessible. Note the annotation allows for overriding the name (and params for methods and constructors).
Property name
ami.db.persister.plugins=comma_delimited_list_of_fully_qualified_java_class_names
Example - Java Code
1package com.demo;
2import com.f1.ami.web.AmiScriptAccessible;
3
4@AmiScriptAccessible(name = "TestAccount")
5public class TestClass {
6 private double price;
7 private int quantity;
8 private String name;
9
10 @AmiScriptAccessible
11 public TestClass(String name) { this.name = name; }
12
13 @AmiScriptAccessible(name = "setValue", params = { "px", "qty" })
14 public void setValue(double price, int quantity) {
15this.price = price;
16this.quantity = quantity;
17}
18
19 @AmiScriptAccessible(name = "describe")
20 public String describe() {
21 return quantity + "@" + price + " for " + name + " is " + (quantity * price);
22 }
23}
Example - Configuration
amiscript.custom.classes=com.demo.TestClass
Example - AmiScript
TestAccount myAccount=new TestAccount("ABC");
myAccount.setValue(40.5,1000);
session.log(myAccount.describe());
GUI Extensions (AMI One, AMI Web)
Overview
AMI's frontend is broken up into "panels". Each panel has a particular type, ex: table, chart, heatmap, etc. You can implement your own panel types using the Web Panel plugin interface.
The Web Panel Plugin is a factory which generates AmiWebPluginPortlet (which represent an instance of a panel)
Java interface (see javadoc for details)
com.f1.ami.web.AmiWebPanelPlugin
Property name
ami.web.panels=comma_delimited_list_of_fully_qualified_java_class_names
Data Access Control Plugin
Overview
This plugin control at a granular level what data a user can see. Here are the steps:
1. A user is successfully logged in, determined by
com.f1.ami.web.auth.AmiAuthenticator plugin which can return as set of variables that are assigned to the user's session (these variables often come from some external corporate entitlements system).
2. This user-session is passed into the
com.f1.ami.web.datafilter.AmiWebDataFilterPlugin which then returns a com.f1.ami.web.datafilter.AmiWebDataFilterinstance. Note that each user will generally have there "own" AmiWebDataFilter assigned to there session.
3. As data is passed from the backend to the frontend its is first visited by the user's AmiWebDataFilter where the DataFilter can choose to suppress the data or not. There are two distinct ways data can be transferred from the "backend" to the user:
a. Realtime - As data is streamed into AMI, individual records are transported to the front end for display on a per-row basis. More specifically as rows are added*, updated** and deleted from the backend a corresponding message is sent to the frontend.
* See AmiWebDataFilter::evaluateNewRow(...)
** See AmiWebDataFilter::evaluateUpdateRow(...)
(Note, that deletes do not have a callback as it is not applicable for data filtering)
b. Query results - when the user invokes a query (generally via the EXECUTE command within a datamodel) a query object is constructed and sent back to the back end for execution*. Then, the backend responds with a table (or multiple tables) of data**.
*See AmiWebDataFilter::evaluateQueryRequest(...)
**See AmiWebDataFilter::evaluateQueryResponse(...)
Java interface
com.f1.ami.web.datafilter.AmiWebDataFilterPlugin
com.f1.ami.web.datafilter.AmiWebDataFilter
Property name
ami.web.data.filter.plugin.class=fully_qaulified_class_name
Example - Configuration
ami.web.data.filter.plugin.class=com.mysamples.SampleDataFilterPlugin
Example - Java Code
1package com.mysamples;
2
3import com.f1.ami.web.datafilter.AmiWebDataFilter;
4import com.f1.ami.web.datafilter.AmiWebDataFilterPlugin;
5import com.f1.ami.web.datafilter.AmiWebDataSession;
6import com.f1.container.ContainerTools;
7import com.f1.utils.PropertyController;
8
9public class SampleDataFilterPlugin implements AmiWebDataFilterPlugin {
10
11 @Override
12 public void init(ContainerTools tools, PropertyController props) {
13 }
14
15 @Override
16 public String getPluginId() {
17 return "DATAFILTER_PLUGIN";
18 }
19
20 @Override
21 public AmiWebDataFilter createDataFilter(AmiWebDataSession session) {
22 return new SampleDataFilter(session);
23 }
24}
25
26package com.mysamples;
27
28import com.f1.ami.web.AmiWebObject;
29import com.f1.ami.web.datafilter.AmiWebDataFilter;
30import com.f1.ami.web.datafilter.AmiWebDataFilterQuery;
31import com.f1.ami.web.datafilter.AmiWebDataSession;
32import com.f1.base.Column;
33import com.f1.base.Row;
34import com.f1.utils.structs.table.columnar.ColumnarTable;
35
36public class SampleDataFilter implements AmiWebDataFilter {
37
38 private AmiWebDataSession userSession;
39 private String allowedRegion;
40
41 public SampleDataFilter(AmiWebDataSession session) {
42 this.userSession = session;
43 allowedRegion = (String) userSession.getVariableValue("region");
44 if(allowedRegion==null)
45 throw new RuntimeException("no 'region' specified");
46 }
47
48 @Override
49 public byte evaluateNewRow(AmiWebObject realtimeRow) {
50 String region = (String) realtimeRow.getParam("region");
51 return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
52 }
53
54 @Override
55 public byte evaluateUpdatedRow(AmiWebObject realtimeRow, byte currentStatus) {
56 Object region = realtimeRow.getParam("region");
57 return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
58 }
59
60 @Override
61 public AmiWebDataFilterQuery evaluateQueryRequest(AmiWebDataFilterQuery query) {
62 return query;
63 }
64
65 @Override
66 public voidevaluateQueryResponse(AmiWebDataFilterQuery query, ColumnarTable table) {
67 Column regionColumn = table.getColumnsMap().get("region");
68 if (regionColumn == null)
69 return;
70 for (int i = table.getSize() - 1; i >= 0; i--) {
71 Row row = table.getRow(i);
72 String region = row.getAt(regionColumn.getLocation(), String.class);
73 if (!allowedRegion.equals(region))
74 table.removeRow(row);
75 }
76 }
77}
AmiGuiService
AmiGuiService - A mechanism for bridging AmiScript to/from JavaScript. This plugin enables JavaScript code to be accessible from AmiScript, through a well-defined AmiScript API. It's bidirectional:
- Calls to JavaScript from within the browser can, in turn, invoke AmiScript callbacks
- Calls to AmiScript from within the dashboard can, in turn, call JavaScript within the browser.
How this works
In order to interact with AmiScript in the webserver and with JavaScript in the web browser, two blocks of adapter code must be written. Each of these two blocks of code are started as singletons which communicate over the AMI HTTP[S] transport (this transport is, however, transparent to you when building these adapters). The webserver's singleton is one per user session and the JavaScript singleton object is started when the browser loads. These two objects are responsible for communication between their respective environments and each other:
Deep Dive
Initializing
- First, a Java plugin implementing the com.f1.ami.web.guiplugin.AmiWebGuiServicePlugin is initiated when the Ami Web Server starts up. The class name must be specified in the ami.guiservice.plugins property. Note, that only one instance is started up per JVM.
- Each time a user logs in, the AmiWebGuiServicePlugin::createGuiIntegrationAdapter is called which returns a custom class implementing the AmiWebGuiServiceAdapter interface.
- Each time the page is refreshed, including on initial login, the following methods are called on the AmiWebGuiServiceAdapter (These methods initialize the browser's JavaScript environment):
- getJavascriptLibraries() -> Your adapter returns a list of libraries to load. This tells the browser which libraries to load, if any, for this custom adapter.
- getJavascriptInitialization() -> Your adapter should return JavaScript to be executed in the browser at pageLoad and allows for any custom initialization.
- getJavascriptNewInstance() -> Your adapter must return the JavaScript necessary for generating a JavaScript singleton that will receive/send messages. This JavaScript must implement a function called registerAmiGuiServicePeer(peer) which, typically, just stores the peer argument in a member variable for later use.