Difference between revisions of "AMI Custom Java Plugins"

From 3forge Documentation
Jump to navigation Jump to search
 
(25 intermediate revisions by 7 users not shown)
Line 35: Line 35:
 
* 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)
 
* 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)
 
* 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 ==
 
== AMI Predefined Attributes ==
 
{| class="wikitable"
 
{| class="wikitable"
Line 102: Line 103:
 
        @Override
 
        @Override
 
        public AmiAuthResponse authenticate(String namespace, String location, String user, String password) {
 
        public AmiAuthResponse authenticate(String namespace, String location, String user, String password) {
                 final List<AmiAuthAttribute> attributes = new ArrayList<AmiAuthAttribute>();
+
                 final Map<String, Object> attributes = new HashMap<>();
                 attributes.add(new BasicAmiAttribute("ISDEV", "false")); // Set to true for developer privileges  
+
                 attributes.put("ISDEV","false"); // Set to true for developer privileges  
                 attributes.add(new BasicAmiAttribute("ISADMIN", "false")); // Set to true for admin privileges  
+
                 attributes.put("ISADMIN", "false"); // Set to true for admin privileges  
                 attributes.add(new BasicAmiAttribute("DEFAULT_LAYOUT", "default_layout.ami"));
+
                 attributes.put("DEFAULT_LAYOUT", "default_layout.ami");
                 attributes.add(new BasicAmiAttribute("LAYOUTS", "layout1.ami,layout2.ami"));
+
                 attributes.put("LAYOUTS", "layout1.ami,layout2.ami");
  
 
                 Map<String, Object> allowedWindows = new HashMap<String, Object>();
 
                 Map<String, Object> allowedWindows = new HashMap<String, Object>();
 
                 allowedWindows.put("namespace1", new HashSet(Arrays.asList("Window1PNL", "Window2PNL")));
 
                 allowedWindows.put("namespace1", new HashSet(Arrays.asList("Window1PNL", "Window2PNL")));
                 attributes.add(new BasicAmiAttribute("amiscript.variable.allowedWindows", allowedWindows)); // This adds a custom AMI Session Variable called `allowedWindows` which will then be used in some custom script to control which windows are visible
+
                 attributes.put("amiscript.variable.allowedWindows", allowedWindows); // This adds a custom AMI Session Variable called `allowedWindows` which will then be used in some custom script to control which windows are visible
                 attributes.add(new BasicAmiAttribute("amiscript.variable.env", "UAT")); // This adds a custom AMI Session Variable called `env` to "UAT"
+
                 attributes.put("amiscript.variable.env", "UAT"); // This adds a custom AMI Session Variable called `env` to "UAT"
 
                  
 
                  
 
                 // Use AmiAuthResponse.STATUS_GENERAL_ERROR if authentication failed.
 
                 // Use AmiAuthResponse.STATUS_GENERAL_ERROR if authentication failed.
                 return new BasicAmiAuthResponse(AmiAuthResponse.STATUS_OKAY, "Message", new BasicAmiAuthUser(username, "Jackie", "Davenson", "777-888-9999", "jDavenson@mail.com", "Tire Co.", attributes));
+
                 return new BasicAmiAuthResponse(AmiAuthResponse.STATUS_OKAY, null, new BasicAmiAuthUser(user, attributes));
 
        }
 
        }
  
Line 150: Line 151:
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
= AmiWebSSOPlugin for Single Sign On or Federated Authentication =
 +
== Overview ==
 +
It is necessary to authenticate users to ensure that they have valid credentials and have been granted access to your application. 3forge AMI has support for SSO Plugins. SSO Plugins are authentication plugins for 3forge AMI Applications that need to support either Single Sign On or Federated Identity, where SSO provides support for single access to multiple systems in one organization and Federated Identity provides that for multiple organizations.<br>
 +
 +
This document will provide the basic template you will need to set up your own Java 3forge AMI SSO Plugin.<br>
 +
 +
You will need to implement a Java interface and include the properties provided below.<br>
 +
 +
[[File:AMIWebSSO1.jpg]]<br>
 +
 +
== Java interface ==
 +
<syntaxhighlight lang="java">
 +
com.f1.ami.web.AmiWebSSOPlugin
 +
 +
package com.f1.ami.web;
 +
 +
import com.f1.ami.amicommon.AmiPlugin;
 +
import com.f1.ami.web.auth.AmiAuthUser;
 +
import com.f1.http.HttpRequestResponse;
 +
import com.f1.suite.web.HttpRequestAction;
 +
 +
/**
 +
* 1) Set the sso.plugin.class property to point to an implementation of this class
 +
* 2) Set the ami.web.index.html.file property to the URL that is associated with buildAuthRequest(...) method
 +
* 3) getExpectedResponsePath() must return URL associated with processResponse(...) method
 +
* 4) When users access the index file (from step 1) buildAuthRequest(..) is called and the user's browser is redirected to the returned URL (usually the IDP)
 +
* 5) After the IDP has authenticated, the IDP should then redirect the user's browser to the getExpectedResposnePath() URL (from step 2)
 +
* 6) processResponse(...) is called and should return an AmiUathUser that will be passed to the AMI dashboard
 +
*/
 +
public interface AmiWebSSOPlugin extends AmiPlugin {
 +
 +
  /**
 +
  *
 +
  * Use the ami.web.index.html.file to associate the URL that will cause this method to be invoked. This method should inspect the HTTP request and formulate a fully qualified
 +
  * URL that will be sent to the IDP.
 +
  * Note, thistask method should return the URL as specified in getExpectedResponsePath() if it's determined,based on the supplied request that the user is already authenticated
 +
  *
 +
  * @param req
 +
  *            the http request
 +
  * @return a URL that the user's browser will be redirected to
 +
  */
 +
  String buildAuthRequest(HttpRequestResponse req) throws Exception;
 +
 +
  /**
 +
  * @return the URL that the processResponse(...) method is associated with. This method is called once at startup. Hence, return value is really a constant
 +
  */
 +
  String getExpectedResponsePath();
 +
 +
  /**
 +
  *
 +
  * @param req
 +
  *            the http request
 +
  * @return null if not allowed. See com.f1.ami.web.auth.BasicAmiAuthUser for convenience class
 +
  * @throws Exception
 +
  *            if there was an error, the user will not be permitted to login
 +
  */
 +
  AmiAuthUser processResponse(HttpRequestAction req) throws Exception;
 +
 +
}
 +
</syntaxhighlight>
 +
 +
== Associated Properties ==
 +
<pre>
 +
sso.plugin.class=fully_qualified_class_name
 +
ami.web.index.html.file=index2.htm
 +
web.logged.out.url=/loggedout.htm
 +
</pre>
 +
 +
== Example ==
 +
(1). Example Java-Code <br>
 +
<syntaxhighlight lang="java">
 +
package com.company.ami;
 +
 +
import java.util.HashMap;
 +
import java.util.Map;
 +
 +
import com.f1.ami.web.auth.AmiAuthUser;
 +
import com.f1.ami.web.auth.BasicAmiAuthUser;
 +
import com.f1.container.ContainerTools;
 +
import com.f1.http.HttpRequestResponse;
 +
import com.f1.http.HttpSession;
 +
import com.f1.suite.web.HttpRequestAction;
 +
import com.f1.utils.PropertyController;
 +
 +
public class MySSOPlugin implements AmiWebSSOPlugin {
 +
 +
    @Override
 +
    public void init(ContainerTools tools, PropertyController props) {
 +
        // TODO Auto-generated method stub
 +
        System.out.println("Initializing My SSO Plugin");
 +
    }
 +
 +
    @Override
 +
    public String getPluginId() {
 +
        return "SampleSSOPlugin";
 +
    }
 +
 +
    @Override
 +
    public String buildAuthRequest(HttpRequestResponse req) throws Exception {
 +
        HttpSession session = req.getSession(true);
 +
        String requestUri = req.getRequestUri(); // This will be your ami.web.index.html.file
 +
        Map<String, String> header = req.getHeader();
 +
        Map<String, String> cookies = req.getCookies();
 +
        Map<String, String> params = req.getParams();
 +
 +
        // 1) Here you build your Authorization Request for your Identity Provider
 +
        // ... Code here
 +
 +
        // This is how you can add a cookie to the response
 +
        String optionalDomain = null;
 +
        long optionalExpires = 0;
 +
        req.putCookie("myCookie", "secretCode", optionalDomain, optionalExpires, null);
 +
 +
        // This is how you add a header to the response
 +
        req.putResponseHeader("myHeader", "value");
 +
 +
        String redirectUrlForLogin = "https://identityProvider/login?{login parameters}&expectedResponsePath=" + this.getExpectedResponsePath();
 +
        //// End code
 +
        return redirectUrlForLogin;
 +
    }
 +
 +
    @Override
 +
    public AmiAuthUser processResponse(HttpRequestAction req) throws Exception {
 +
        HttpRequestResponse request = req.getRequest();
 +
        Map<String, String> header = request.getHeader();
 +
        Map<String, String> cookies = request.getCookies();
 +
        Map<String, String> params = request.getParams();
 +
 +
        // 2) Ensure Identity Provider Authorized the User
 +
        // ... Code here
 +
        ////
 +
 +
        // If the user is valid:
 +
        // 3) Add user attributes to authAttributes;
 +
        Map<String, Object> authAttributes = new HashMap<String, Object>();
 +
        // ... Code here
 +
        ////
 +
        return new BasicAmiAuthUser("username", authAttributes);
 +
    }
 +
 +
    @Override
 +
    public String getExpectedResponsePath() {
 +
        return "login_redirect_url";
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
(2). Example Configuration <br>
 +
<pre>
 +
sso.plugin.class=com.company.ami.MySSOPlugin
 +
ami.web.index.html.file=index.htm
 +
web.logged.out.url=/loggedout.htm
 +
</pre>
 +
 +
==Reserved Paths==
 +
'''NOTE''': Below is a list of url paths reserved for use by 3forge.<br>
 +
Avoid using any of them as your expected response path to avoid unexpected behavior.<br>
 +
*<span style="color: red;">3forge_hello</span> (e.g. hostname:33332/3forge_hello)
 +
*<span style="color: red;">3forge_goodbye</span>
 +
*<span style="color: red;">3forge_sessions</span>
 +
*<span style="color: red;">own_headless</span>
 +
*<span style="color: red;">logout</span>
 +
*<span style="color: red;">login</span>
 +
*<span style="color: red;">resources</span>
 +
*<span style="color: red;">run</span>
 +
*<span style="color: red;">modcount</span>
 +
*<span style="color: red;">get_custom_login_image</span>
  
 
= Connections to Custom External Datasources (AMI One, Center) =
 
= Connections to Custom External Datasources (AMI One, Center) =
Line 522: Line 691:
 
import com.f1.container.ContainerTools;
 
import com.f1.container.ContainerTools;
 
import com.f1.utils.PropertyController;
 
import com.f1.utils.PropertyController;
 +
import com.f1.ami.center.timers.AmiTimerFactory;
 +
import com.f1.ami.center.timers.AmiTimer;
  
 
public class TestTimerFactory implements AmiTimerFactory {
 
public class TestTimerFactory implements AmiTimerFactory {
Line 548: Line 719:
 
       @Override
 
       @Override
 
       public String getPluginId() {
 
       public String getPluginId() {
               return "TESTTIMER";
+
               return "MyStartupTimer";
 
       }
 
       }
 
}
 
}
Line 555: Line 726:
  
 
import com.f1.ami.center.table.AmiImdb;
 
import com.f1.ami.center.table.AmiImdb;
 +
import com.f1.ami.center.timers.AmiAbstractTimer;
  
 
public class TestTimer extends AmiAbstractTimer {
 
public class TestTimer extends AmiAbstractTimer {
  
 
       @Override
 
       @Override
       public void onTimer(long scheduledTime) {
+
       public void onTimer(long scheduledTime, AmiImdbSession sesssion, AmiCenterProcess process) {
               // TODOAuto-generated method stub
 
       }
 
 
 
       @Override
 
       public void onSchemaChanged(AmiImdb imdb) {
 
 
               // TODOAuto-generated method stub
 
               // TODOAuto-generated method stub
 
       }
 
       }
  
 
       @Override
 
       @Override
       protected void onStartup() {
+
       protected void onStartup(AmiImdbSession timerSession) {
 
               // TODOAuto-generated method stub
 
               // TODOAuto-generated method stub
 
       }
 
       }
Line 745: Line 912:
 
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.
 
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).
+
The class must have the com.f1.ami.amicommon.customobjects.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 ==
 
== Property name ==
ami.db.persister.plugins=''comma_delimited_list_of_fully_qualified_java_class_names''  
+
 
 +
Use the first property to make the custom java objects available in amiweb and the second property to make them available in amicenter/amidb.
 +
 
 +
ami.web.amiscript.custom.classes=''comma_delimited_list_of_fully_qualified_java_class_names''
 +
 
 +
ami.center.amiscript.custom.classes=''comma_delimited_list_of_fully_qualified_java_class_names''
  
 
<span style="color: blue;">'''Example - Java Code'''</span>
 
<span style="color: blue;">'''Example - Java Code'''</span>
Line 754: Line 926:
 
<syntaxhighlight lang="java" line="1">
 
<syntaxhighlight lang="java" line="1">
 
package com.demo;
 
package com.demo;
import com.f1.ami.web.AmiScriptAccessible;
+
import com.f1.ami.amicommon.customobjects.AmiScriptAccessible;
  
 
@AmiScriptAccessible(name = "TestAccount")
 
@AmiScriptAccessible(name = "TestAccount")
Line 771: Line 943:
 
}
 
}
  
        @AmiScriptAccessible(name = "describe")
+
        @AmiScriptAccessible(name = "print")
        public String describe() {
+
        public String print() {
 
                return quantity + "@" + price + " for " + name + " is " + (quantity * price);
 
                return quantity + "@" + price + " for " + name + " is " + (quantity * price);
 
        }
 
        }
Line 780: Line 952:
 
<span style="color: blue;">'''Example - Configuration'''</span>
 
<span style="color: blue;">'''Example - Configuration'''</span>
  
amiscript.custom.classes=com.demo.TestClass
+
ami.web.amiscript.custom.classes=com.demo.TestClass
 +
 
 +
ami.center.amiscript.custom.classes=com.demo.TestClass
  
 
<span style="color: blue;">'''Example - AmiScript'''</span>
 
<span style="color: blue;">'''Example - AmiScript'''</span>
Line 788: Line 962:
 
myAccount.setValue(40.5,1000);
 
myAccount.setValue(40.5,1000);
  
session.log(myAccount.describe());
+
session.log(myAccount.print());
  
 
= GUI Extensions (AMI One, AMI Web) =
 
= GUI Extensions (AMI One, AMI Web) =
Line 864: Line 1,038:
 
        public String getPluginId() {
 
        public String getPluginId() {
 
                return "DATAFILTER_PLUGIN";
 
                return "DATAFILTER_PLUGIN";
 +
        }
 +
 +
        @Override
 +
        public void onLogin() {
 +
                //Code to implement when the user logs in;
 +
        }
 +
 +
        @Override
 +
        public void onLogout() {
 +
                //Code to implement when the user logs out;
 
        }
 
        }
  
Line 902: Line 1,086:
 
        @Override
 
        @Override
 
        public byte evaluateUpdatedRow(AmiWebObject realtimeRow, byte currentStatus) {
 
        public byte evaluateUpdatedRow(AmiWebObject realtimeRow, byte currentStatus) {
                Object region = realtimeRow.getParam("region");
+
                Object region = (String) realtimeRow.getParam("region");
 
                return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
 
                return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
 
        }
 
        }
Line 1,127: Line 1,311:
  
 
== Overview ==
 
== Overview ==
The Feed Handler Plugin allows for the very efficient processing incoming streams of data to be transmitted into the AMI Center using AMI's proprietary protocol.  Generally one feed handler will be written per type of messaging bus.
+
The Feed Handler Plugin allows for the very efficient processing of incoming streams of data to be transmitted into the AMI Center using AMI's proprietary protocol.  Generally one feed handler will be written per type of messaging bus.
  
 
== Properties ==
 
== Properties ==
Line 1,439: Line 1,623:
  
 
[[File:CustomRelayFeeder.jpg|frameless|770x770px]]
 
[[File:CustomRelayFeeder.jpg|frameless|770x770px]]
 +
 +
= Custom Relay Plugin =
 +
 +
== Overview ==
 +
The AMI Relay Plugin allows for custom processing of the Realtime Messaging API.
 +
 +
Depending on the return value of processData, the each message is handled as such:
 +
 +
{| class="wikitable"
 +
!Return Value
 +
!AMI Behavior
 +
|-
 +
|ERROR
 +
|''The <span style="color: blue;">errorSink</span> message gets printed, and the incoming message is not processed''
 +
|-
 +
|NA
 +
|''The <span style="color: blue;">mutableRawData</span> value is ignored and the original message is processed by AMI''
 +
|-
 +
|SKIP
 +
|''The incoming message is not processed''
 +
|-
 +
|OKAY
 +
|''The <span style="color: blue;">mutableRawData</span> value is processed by AMI''
 +
|}
 +
 +
== Usage ==
 +
To use the custom relay plugin, place the exported jar file inside the AMI libs directory.
 +
During login to the realtime port, a user can specify a custom relay plugin to use with the syntax:
 +
 +
<span style="font-family: courier new; color: red;">L</span><span style="font-family: courier new; color: blue;">|</span><span style="font-family: courier new; color: red;">I</span><span style="font-family: courier new; color: blue;">="demo"|<span style="font-family: courier new; color: red;">P</span>="fully.qualified.relay.plugin.name"</span>
 +
 +
== AmiRelayPlugin Interface ==
 +
 +
<syntaxhighlight lang="java" line="1">
 +
package com.f1.ami.relay;
 +
 +
import com.f1.ami.relay.fh.AmiFH;
 +
import com.f1.utils.ByteArray;
 +
import com.f1.utils.PropertyController;
 +
 +
public interface AmiRelayPlugin {
 +
    int ERROR = 1;
 +
    int NA = 2;
 +
    int OKAY = 3;
 +
    int SKIP = 4;
 +
 +
    int processData(ByteArray mutableRawData, StringBuilder errorSink);
 +
    boolean init(PropertyController properties, AmiFH fh, String switches, StringBuilder errorSink);
 +
}
 +
</syntaxhighlight>
 +
 +
== Example ==
 +
<syntaxhighlight lang="java" line="1">
 +
package com.f1;
 +
 +
import java.util.Map;
 +
import java.util.Map.Entry;
 +
 +
import com.f1.ami.relay.AmiRelayPlugin;
 +
import com.f1.ami.relay.fh.AmiFH;
 +
import com.f1.utils.AH;
 +
import com.f1.utils.ByteArray;
 +
import com.f1.utils.FastByteArrayDataOutputStream;
 +
import com.f1.utils.PropertyController;
 +
import com.f1.utils.SH;
 +
 +
public class SampleRelayPlugin implements AmiRelayPlugin {
 +
private static final String AMI_SAMPLEPLUGIN_PROP = "ami.sampleplugin.prop";
 +
private byte[] prefix = "SAMPLE|".getBytes();
 +
private final FastByteArrayDataOutputStream buf = new FastByteArrayDataOutputStream();
 +
private String prop;
 +
 +
@Override
 +
public boolean init(PropertyController properties, AmiFH fh, String switches, StringBuilder errorSink) {
 +
prop = properties.getOptional(AMI_SAMPLEPLUGIN_PROP, "|");
 +
if (SH.is(switches)) {
 +
Map<String, String> switchMap = SH.splitToMap(',', '=', '\\', switches);
 +
for (Entry<String, String> e : switchMap.entrySet()) {
 +
final String key = e.getKey();
 +
final String value = e.getValue();
 +
if (AMI_SAMPLEPLUGIN_PROP.equals(key))
 +
prop = (String) value;
 +
else {
 +
errorSink.append("unknown switch: ").append(key);
 +
return false;
 +
}
 +
}
 +
}
 +
return true;
 +
}
 +
 +
@Override
 +
public int processData(ByteArray mutableRawData, StringBuilder errorSink) {
 +
byte[] data = mutableRawData.getData();
 +
if (!AH.startsWith(data, prefix, mutableRawData.getStart()))
 +
return NA;
 +
//Process input data
 +
int pos = mutableRawData.getStart() + prefix.length - 1;
 +
int length = mutableRawData.getEnd();
 +
if (pos >= length) {
 +
errorSink.append("No data to parse");
 +
return ERROR;
 +
}
 +
//Handle complex parsing here...
 +
buf.reset(2048);
 +
buf.writeBytes("O|T=\"Table\"|val=100");
 +
mutableRawData.reset(buf.getBuffer(), 0, buf.getCount());
 +
return OKAY;
 +
}
 +
}
 +
</syntaxhighlight>
  
 
=Property Decrypter=
 
=Property Decrypter=
  
This plugin is used to avoid storing plain text passwords inside properties files. Use the Decrypter interface to implement a customized methodology for decrypting/retrieving data stored in .properties file. After creating a class that implements this interface, add the full class name to the -Df1.properties.decrypters property. As a result, where ever the ${CIPHER:xxx} syntax within .properties files is encountered the xxx will be passed to the decryptString(...) method. If the decryptString is unable to process the xxx, then simply return null or throw an exception.
+
This plugin is used to avoid storing plain text passwords inside properties files. Use the Decrypter interface to implement a customized methodology for decrypting/retrieving data. After creating a class that implements the com.f1.utils.encrypt.Decrypter interface, add the full class name to the -Df1.properties.decrypters property. As a result, where ever the ${CIPHER:xxx} syntax within .properties files is encountered the xxx will be passed to the decryptString(...) method. If the decryptString is unable to process the xxx, then simply return null or throw an exception.
  
 
Note: Instead of using the Decrypter plugin, you can instead use the tools.sh in conjunction with the -Df1.properties.secret.key.files to encrypt tokens manually.
 
Note: Instead of using the Decrypter plugin, you can instead use the tools.sh in conjunction with the -Df1.properties.secret.key.files to encrypt tokens manually.
Line 1,449: Line 1,744:
 
package com.f1.ami.relay;
 
package com.f1.ami.relay;
  
<syntaxhighlight lang="java" line="1">
 
 
package com.f1.utils.encrypt;
 
package com.f1.utils.encrypt;
 
public interface Decrypter {
 
public interface Decrypter {
Line 1,460: Line 1,754:
 
<syntaxhighlight lang="java" line="1">
 
<syntaxhighlight lang="java" line="1">
 
package com.example;
 
package com.example;
 +
import com.f1.utils.encrypt.Decrypter;
  
 
//Add to your java vm arguments: -Df1.properties.decrypters=com.example.MyDecrypter
 
//Add to your java vm arguments: -Df1.properties.decrypters=com.example.MyDecrypter
Line 1,465: Line 1,760:
  
 
   //Normally this would be more sophisticated, like reaching out to a secure vault, etc.
 
   //Normally this would be more sophisticated, like reaching out to a secure vault, etc.
   String decryptString(String encrypted){
+
   public String decryptString(String encrypted){
 
     if("secretPass".equals(encrypted))
 
     if("secretPass".equals(encrypted))
 
       return "password123";
 
       return "password123";
 
     return null;
 
     return null;
 
   }
 
   }
   byte[]decrypt(String encrypted){
+
   public  byte[] decrypt(String encrypted){
 
     String s=decryptString(encrypted);
 
     String s=decryptString(encrypted);
 
     return s==null ? null : s.getBytes();
 
     return s==null ? null : s.getBytes();

Latest revision as of 17:46, 29 November 2023

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 from the cloud directory on login
LAYOUTS A comma delimited list of regular expressions for layouts that are available
ami_layout_shared If set, this will be the default layout loaded from the shared directory on login. This has been deprecated, use DEFAULT_LAYOUT
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.Arrays;
 5import java.util.HashMap;
 6import java.util.HashSet;
 7import java.util.List;
 8import java.util.Map;
 9
10import com.f1.ami.web.auth.AmiAuthAttribute;
11import com.f1.ami.web.auth.AmiAuthResponse;
12import com.f1.ami.web.auth.AmiAuthenticator;
13import com.f1.ami.web.auth.BasicAmiAttribute;
14import com.f1.ami.web.auth.BasicAmiAuthResponse;
15import com.f1.ami.web.auth.BasicAmiAuthUser;
16import com.f1.container.ContainerTools;
17import com.f1.utils.PropertyController;
18
19public class TestAuthenticator implements AmiAuthenticator {
20
21        @Override
22        public void init(ContainerTools tools, PropertyController props) {
23                // TODO Auto-generated method stub
24        }
25
26        @Override
27        public AmiAuthResponse authenticate(String namespace, String location, String user, String password) {
28                final Map<String, Object> attributes = new HashMap<>();
29                attributes.put("ISDEV","false"); // Set to true for developer privileges 
30                attributes.put("ISADMIN", "false"); // Set to true for admin privileges 
31                attributes.put("DEFAULT_LAYOUT", "default_layout.ami");
32                attributes.put("LAYOUTS", "layout1.ami,layout2.ami");
33
34                Map<String, Object> allowedWindows = new HashMap<String, Object>();
35                allowedWindows.put("namespace1", new HashSet(Arrays.asList("Window1PNL", "Window2PNL")));
36                attributes.put("amiscript.variable.allowedWindows", allowedWindows); // This adds a custom AMI Session Variable called `allowedWindows` which will then be used in some custom script to control which windows are visible
37                attributes.put("amiscript.variable.env", "UAT"); // This adds a custom AMI Session Variable called `env` to "UAT"
38                
39                // Use AmiAuthResponse.STATUS_GENERAL_ERROR if authentication failed.
40                return new BasicAmiAuthResponse(AmiAuthResponse.STATUS_OKAY, null, new BasicAmiAuthUser(user, attributes));
41        }
42
43        @Override
44        public String getPluginId() {
45                return "TestAuthenticator";
46        }
47}

Example - Configuration

ami.auth.plugin.class=com.demo.TestAuthenticatorPlugin

Example - Controlling which windows are visible using the onStartup Callback

  • The following is required: Dashboard Settings: User Preferences Namespace*
 1// First, let's find out which dashboard the user has loaded.
 2String layoutNamespace = session.getUserPreferencesNamespace();
 3
 4// Get set of Allowed Windows for the current dashboard, per entitlements.  Note, the allowedWindows map was defined and populated in the entitlements plugin above
 5Set allowedWindowsSet =  allowedWindows.get(layoutNamespace);
 6
 7// Loop through all windows in the dashboard, marking any windows that are not in the entitlments as HIDDEN so the user does not have access to them
 8Map windowsMap = session.getWindowsMap();
 9for(String id: windowsMap.getKeys()){
10  if(allowedWindowsSet == null || !allowedWindowsSet.contains(id)){
11    Window w = windowsMap.get(id);
12    w.setType("HIDDEN");  
13    w.minimize();
14  }
15}

AmiWebSSOPlugin for Single Sign On or Federated Authentication

Overview

It is necessary to authenticate users to ensure that they have valid credentials and have been granted access to your application. 3forge AMI has support for SSO Plugins. SSO Plugins are authentication plugins for 3forge AMI Applications that need to support either Single Sign On or Federated Identity, where SSO provides support for single access to multiple systems in one organization and Federated Identity provides that for multiple organizations.

This document will provide the basic template you will need to set up your own Java 3forge AMI SSO Plugin.

You will need to implement a Java interface and include the properties provided below.

AMIWebSSO1.jpg

Java interface

com.f1.ami.web.AmiWebSSOPlugin

package com.f1.ami.web;
 
import com.f1.ami.amicommon.AmiPlugin;
import com.f1.ami.web.auth.AmiAuthUser;
import com.f1.http.HttpRequestResponse;
import com.f1.suite.web.HttpRequestAction;
 
/**
 * 1) Set the sso.plugin.class property to point to an implementation of this class
 * 2) Set the ami.web.index.html.file property to the URL that is associated with buildAuthRequest(...) method
 * 3) getExpectedResponsePath() must return URL associated with processResponse(...) method
 * 4) When users access the index file (from step 1) buildAuthRequest(..) is called and the user's browser is redirected to the returned URL (usually the IDP)
 * 5) After the IDP has authenticated, the IDP should then redirect the user's browser to the getExpectedResposnePath() URL (from step 2)
 * 6) processResponse(...) is called and should return an AmiUathUser that will be passed to the AMI dashboard
 */
public interface AmiWebSSOPlugin extends AmiPlugin {
 
   	/**
   	 *
   	 * Use the ami.web.index.html.file to associate the URL that will cause this method to be invoked. This method should inspect the HTTP request and formulate a fully qualified
   	 * URL that will be sent to the IDP.
   	 * Note, thistask method should return the URL as specified in getExpectedResponsePath() if it's determined,based on the supplied request that the user is already authenticated
   	 *
   	 * @param req
   	 *            the http request
   	 * @return a URL that the user's browser will be redirected to
   	 */
   	String buildAuthRequest(HttpRequestResponse req) throws Exception;
 
   	/**
   	 * @return the URL that the processResponse(...) method is associated with. This method is called once at startup. Hence, return value is really a constant
   	 */
   	String getExpectedResponsePath();
 
   	/**
   	 *
   	 * @param req
   	 *            the http request
   	 * @return null if not allowed. See com.f1.ami.web.auth.BasicAmiAuthUser for convenience class
   	 * @throws Exception
   	 *             if there was an error, the user will not be permitted to login
   	 */
   	AmiAuthUser processResponse(HttpRequestAction req) throws Exception;
 
}

Associated Properties

sso.plugin.class=fully_qualified_class_name
ami.web.index.html.file=index2.htm
web.logged.out.url=/loggedout.htm

Example

(1). Example Java-Code

package com.company.ami;
 
import java.util.HashMap;
import java.util.Map;
 
import com.f1.ami.web.auth.AmiAuthUser;
import com.f1.ami.web.auth.BasicAmiAuthUser;
import com.f1.container.ContainerTools;
import com.f1.http.HttpRequestResponse;
import com.f1.http.HttpSession;
import com.f1.suite.web.HttpRequestAction;
import com.f1.utils.PropertyController;
 
public class MySSOPlugin implements AmiWebSSOPlugin {
 
    @Override
    public void init(ContainerTools tools, PropertyController props) {
        // TODO Auto-generated method stub
        System.out.println("Initializing My SSO Plugin");
    }
 
    @Override
    public String getPluginId() {
        return "SampleSSOPlugin";
    }
 
    @Override
    public String buildAuthRequest(HttpRequestResponse req) throws Exception {
        HttpSession session = req.getSession(true);
        String requestUri = req.getRequestUri(); // This will be your ami.web.index.html.file
        Map<String, String> header = req.getHeader();
        Map<String, String> cookies = req.getCookies();
        Map<String, String> params = req.getParams();
 
        // 1) Here you build your Authorization Request for your Identity Provider
        // ... Code here
 
        // This is how you can add a cookie to the response
        String optionalDomain = null;
        long optionalExpires = 0;
        req.putCookie("myCookie", "secretCode", optionalDomain, optionalExpires, null);
 
        // This is how you add a header to the response
        req.putResponseHeader("myHeader", "value");
 
        String redirectUrlForLogin = "https://identityProvider/login?{login parameters}&expectedResponsePath=" + this.getExpectedResponsePath();
        //// End code
        return redirectUrlForLogin;
    }
 
    @Override
    public AmiAuthUser processResponse(HttpRequestAction req) throws Exception {
        HttpRequestResponse request = req.getRequest();
        Map<String, String> header = request.getHeader();
        Map<String, String> cookies = request.getCookies();
        Map<String, String> params = request.getParams();
 
        // 2) Ensure Identity Provider Authorized the User
        // ... Code here
        ////
 
        // If the user is valid:
        // 3) Add user attributes to authAttributes;
        Map<String, Object> authAttributes = new HashMap<String, Object>();
        // ... Code here
        ////
        return new BasicAmiAuthUser("username", authAttributes);
    }
 
    @Override
    public String getExpectedResponsePath() {
        return "login_redirect_url";
    }
}

(2). Example Configuration

sso.plugin.class=com.company.ami.MySSOPlugin
ami.web.index.html.file=index.htm
web.logged.out.url=/loggedout.htm

Reserved Paths

NOTE: Below is a list of url paths reserved for use by 3forge.
Avoid using any of them as your expected response path to avoid unexpected behavior.

  • 3forge_hello (e.g. hostname:33332/3forge_hello)
  • 3forge_goodbye
  • 3forge_sessions
  • own_headless
  • logout
  • login
  • resources
  • run
  • modcount
  • get_custom_login_image

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:

  1. Look for a registered AmiTriggerFactory with the id "MyRiskCalc".  
  2. Call newTrigger()  on the factory.
  3. 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:

  1. Look for a registered AmiStoredProcFactory with the id "MyCustProc".  
  2. Call newStoredProc()  on the factory.
  3. 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:

  1. Look for a registered AmiStoredProcFactory with the id "MyStartupTimer".  
  2. Call newTimer()  on the factory.  
  3. 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;
10import com.f1.ami.center.timers.AmiTimerFactory;
11import com.f1.ami.center.timers.AmiTimer;
12
13public class TestTimerFactory implements AmiTimerFactory {
14
15       private List<AmiFactoryOption> options = new ArrayList<AmiFactoryOption>();
16
17       public TestTimerFactory() {
18               // TODOAuto-generated method stub
19       }
20
21       @Override
22       public void init(ContainerTools tools, PropertyController props) {
23               // TODOAuto-generated method stub
24       }
25
26       @Override
27       public Collection<AmiFactoryOption> getAllowedOptions() {
28               return options;
29       }
30
31       @Override
32       public AmiTimer newTimer() {
33               return new TestTimer();
34       }
35
36       @Override
37       public String getPluginId() {
38               return "MyStartupTimer";
39       }
40}
41
42package com.demo;
43
44import com.f1.ami.center.table.AmiImdb;
45import com.f1.ami.center.timers.AmiAbstractTimer;
46
47public class TestTimer extends AmiAbstractTimer {
48
49       @Override
50       public void onTimer(long scheduledTime, AmiImdbSession sesssion, AmiCenterProcess process) {
51               // TODOAuto-generated method stub
52       }
53
54       @Override
55       protected void onStartup(AmiImdbSession timerSession) {
56               // TODOAuto-generated method stub
57       }
58}

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:

  1. Create a table (named "mytable") with the specified columns (col1)
  2. Look for a registered AmiTablePersisterFactory with the id "MyPersister".
  3. Call newPersister()  on the factory, passing in a map of supplied options (myoption=some_val)
  4. 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.amicommon.customobjects.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

Use the first property to make the custom java objects available in amiweb and the second property to make them available in amicenter/amidb.

ami.web.amiscript.custom.classes=comma_delimited_list_of_fully_qualified_java_class_names

ami.center.amiscript.custom.classes=comma_delimited_list_of_fully_qualified_java_class_names

Example - Java Code

 1package com.demo;
 2import com.f1.ami.amicommon.customobjects.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 = "print")
20        public String print() {
21                return quantity + "@" + price + " for " + name + " is " + (quantity * price);
22        }
23}

Example - Configuration

ami.web.amiscript.custom.classes=com.demo.TestClass

ami.center.amiscript.custom.classes=com.demo.TestClass

Example - AmiScript

TestAccount myAccount=new TestAccount("ABC");

myAccount.setValue(40.5,1000);

session.log(myAccount.print());

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_qualified_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 void onLogin() {
22                //Code to implement when the user logs in;
23        }
24
25        @Override
26        public void onLogout() {
27                //Code to implement when the user logs out;
28        }
29
30        @Override
31        public AmiWebDataFilter createDataFilter(AmiWebDataSession session) {
32                return new SampleDataFilter(session);
33        }
34}
35
36package com.mysamples;
37
38import com.f1.ami.web.AmiWebObject;
39import com.f1.ami.web.datafilter.AmiWebDataFilter;
40import com.f1.ami.web.datafilter.AmiWebDataFilterQuery;
41import com.f1.ami.web.datafilter.AmiWebDataSession;
42import com.f1.base.Column;
43import com.f1.base.Row;
44import com.f1.utils.structs.table.columnar.ColumnarTable;
45
46public class SampleDataFilter implements AmiWebDataFilter {
47
48        private AmiWebDataSession userSession;
49        private String allowedRegion;
50
51        public SampleDataFilter(AmiWebDataSession session) {
52                this.userSession = session;
53                allowedRegion = (String) userSession.getVariableValue("region");
54                if(allowedRegion==null)
55                        throw new RuntimeException("no 'region' specified");
56        }
57
58        @Override
59        public byte evaluateNewRow(AmiWebObject realtimeRow) {
60                String region = (String) realtimeRow.getParam("region");
61                return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
62        }
63
64        @Override
65        public byte evaluateUpdatedRow(AmiWebObject realtimeRow, byte currentStatus) {
66                Object region = (String) realtimeRow.getParam("region");
67                return allowedRegion.equals(region) ? SHOW_ALWAYS : HIDE_ALWAYS;
68        }
69
70        @Override
71        public AmiWebDataFilterQuery evaluateQueryRequest(AmiWebDataFilterQuery query) {
72                return query;
73        }
74
75        @Override
76        public void evaluateQueryResponse(AmiWebDataFilterQuery query, ColumnarTable table) {
77                Column regionColumn = table.getColumnsMap().get("region");
78                if (regionColumn == null)
79                        return;
80                for (int i = table.getSize() - 1; i >= 0; i--) {
81                        Row row = table.getRow(i);
82                        String region = row.getAt(regionColumn.getLocation(), String.class);
83                        if (!allowedRegion.equals(region))
84                                table.removeRow(row);
85                }
86        }
87}

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:


AmiGuiService.jpg

Deep Dive

Initializing

  1. 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.
  2. Each time a user logs in, the AmiWebGuiServicePlugin::createGuiIntegrationAdapter is called which returns a custom class implementing the AmiWebGuiServiceAdapter interface.
  3. 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):
Methods Description
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.

Registering AmiScript API

  • Declaring methods that can be called from AmiScript.

Within the AmiScript environment a single object will be available that represents the singleton. Similar to the session object which implements the Session AmiScript class, this object will have a predetermined name, class name and available methods. Here are how those are defined:

Methods Description
AmiWebGuiServiceAdapter:: getAmiscriptClassname() Your adapter should returns the name of the class that is represented by the singleton (analagous to Session).  The singleton object will have the same name but prefixed with two underbars (__).
AmiWebGuiServiceAdapter::getAmiScriptMethods() Your adapter should returns a list of methods that can be called on the custom singleton
  • Declaring AmiScript callbacks.

The callbacks allow for dashboard developers to declare their own AmiScript that gets executed when the callback is invoked. The dashboard developers can edit the callbacks under dashboard → Custom Gui Service Callbacks → <Your custom service>

Method Description
AmiWebGuiServiceAdapter:: getAmiScriptCallbacks() Your adapter should return a list of callbacks available for overriding.

Binding it together

  • AmiScript to Javascript (See Registering AmiScript API.1 for registering methods that can be called in AmiScript):
    1. A user invokes a AmiScript method on your custom singleton.
    2. When a dashboard developer calls the custom AmiScript API you've provided in step (a), then AmiWebGuiServiceAdapter::onAmiScriptMethod is called.  This allows for you to do any validation, data massaging, etc before sending off to JavaScript.
    3. Call executeJavascriptCallback on the supplied peer.  Here is an example of steps 2 and 3, skipping validation, data massaging: public Object onAmiScriptMethod(String name, Object[] args, AmiWebGuiServiceAdapterPeer peer) {peer.executeJavascriptCallback(name, args);return null;}
    4. The Javascript singleton (as defined in Initializing.3.C) will have the method and arguments called on it
  • JavaScript to AmiScript (See Registering AmiScript API.2 for registering callbacks inside AmiScript):
    1. A JavaScript function is called on the singleton.
    2. The JavaScript singleton's method should then call: peer.sendToServer(methodName,arg1,arg2,...); (Note: the peer is supplied on startup in the registerAmiGuiServicePeer function)
    3. The backend Java adapter's AmiWebGuiServiceAdapter:: onCallFromJavascript(…) is called. At this point, your adapter can do validation, data massaging, etc. before passing off to AmiScript
    4. Call executeAmiScriptCallback on the supplied peer. Here is an example of steps 3 and 4, skipping validation, data massaging, etc: public void onCallFromJavascript(String name, Object args[], AmiWebGuiServiceAdapterPeer peer) { peer.executeAmiScriptCallback(name, args);}
    5. AmiScript call back is executed

Full Example

The following example shows a simple example of calling a method on JavaScript from AmiScript, and invoking an AmiScript callback from JavaScript.

Specifically, the example declares an AmiScript custom object called __GuiSample of type GuiSample with a single method called getDate(String) and a single callback called onDateDetermined(…). When the getDate(…) AmiScript is executed, the user is presented with a native JavaScript alert and then the current date from the browser is sent back to the AmiScript via the onDateDetermined(..) callback.

Note, that in this simple example the JavaScript class is defined inside the getJavascriptInitialization() method but generally this would be declared in a dedicated JavaScript library and loaded via getJavascriptLibraries().

local.properties:

ami.guiservice.plugins=samples.AmiWebGuiServicePlugin_Sample

AmiWebGuiServicePlugin_Sample.java:

 1package samples;
 2
 3import com.f1.ami.web.AmiWebGuiServiceAdapter;
 4import com.f1.ami.web.AmiWebGuiServicePlugin;
 5import com.f1.ami.web.AmiWebService;
 6import com.f1.container.ContainerTools;
 7import com.f1.utils.PropertyController;
 8
 9public class AmiWebGuiServicePlugin_Sample implements AmiWebGuiServicePlugin {
10
11        @Override
12        public void init(ContainerTools tools, PropertyController props) {
13        }
14
15        @Override
16        public String getPluginId() {
17                return "GUISAMPLE";
18        }
19
20        @Override
21        public AmiWebGuiServiceAdapter createGuiIntegrationAdapter(AmiWebService service) {
22                return new AmiWebGuiServiceAdapter_Sample();
23        }
24}

AmiWebGuiServiceAdapter_Sample.java:

 1package samples;
 2
 3import java.util.Collections;
 4import java.util.List;
 5
 6import com.f1.ami.web.AmiWebGuiServiceAdapter;
 7import com.f1.ami.web.AmiWebGuiServiceAdapterPeer;
 8import com.f1.utils.CH;
 9import com.f1.utils.structs.table.derived.ParamsDefinition;
10
11public class AmiWebGuiServiceAdapter_Sample implements AmiWebGuiServiceAdapter {
12
13        private AmiWebGuiServiceAdapterPeer peer;
14
15        @Override
16        public void init(AmiWebGuiServiceAdapterPeer peer) {
17                this.peer = peer;
18        }
19
20        @Override
21        public String getGuiServiceId() {
22                return "GUISAMPLE";
23        }
24
25        @Override
26        public String getDescription() {
27                return "Gui Sample";
28        }
29
30        @Override
31        public String getAmiscriptClassname() {
32                return "GuiSample";
33        }
34
35        @Override
36        public List<ParamsDefinition> getAmiscriptMethods() {
37                return CH.l(new ParamsDefinition("getDate", Object.class, "String message"));
38        }
39
40        @Override
41        public Object onAmiScriptMethod(String name, Object[] args) {
42                this.peer.executeJavascriptCallback(name, args);
43                return null;
44        }
45
46        @Override
47        public List<ParamsDefinition> getAmiScriptCallbacks() {
48                return CH.l(new ParamsDefinition("onDateDetermined", Object.class, "String value"));
49        }
50
51        @Override
52        public void onCallFromJavascript(String name, Object args[]) {
53                this.peer.executeAmiScriptCallback(name, args);
54        }
55
56        @Override
57        public String getJavascriptInitialization() {
58                StringBuilder js = new StringBuilder();
59                js.append("function GuiSampleJavascript(){}\n");
60               
61 js.append("GuiSampleJavascript.prototype.registerAmiGuiServicePeer=function(peer){this.peer=peer;}\n");
62             
63 js.append("GuiSampleJavascript.prototype.getDate=function(message){alert(message);this.peer.sendToServer('onDateDetermined',Date.now());}\n");
64                return js.toString();
65        }
66
67        @Override
68        public String getJavascriptNewInstance() {
69                return "new GuiSampleJavascript()";
70        }
71
72        @Override
73        public List<String> getJavascriptLibraries() {
74                return Collections.EMPTY_LIST;
75        }
76}

Inside the dashboard, create a new html panel, add a button and set the button's script to:

__GuiSample.getDate("Getting your local date!");

Inside the dashboard, set the AmScript under Dashboard -> Custom Gui Service Callbacks -> Gui Sample Callbacks... -> onDateDetermined tab to the following:

session.alert("Browser's timestamp is: "+ value);

Running the example:

Clicking the button, will now generate a JavaScript alert saying "Getting your local date!" and then an AmiScript alert displaying "Browser's timestamp is: <unixtimestamp>"

Custom Relay Feed Handler

Overview

The Feed Handler Plugin allows for the very efficient processing of incoming streams of data to be transmitted into the AMI Center using AMI's proprietary protocol.  Generally one feed handler will be written per type of messaging bus.

Properties

Setting up properties in the relay to instantiate a feed handler. In this example, the feed handler will have the name my_feedhandler_name:

ami.relay.fh.active=$${ami.relay.fh.active},my_feedhandler_name

ami.relay.fh.my_feedhandler_name.start=true

ami.relay.fh.my_feedhandler_name.class=class.that.extends.com.f1.ami.relay.AmiFHBase

ami.relay.fh.my_feedhandler_name.props.my_custom_property=my_cutom_value

Feed Handler Interface

You will implement this interface, only this interface in most cases.  Please note that an AmiRelayIn will be passed to the init function, and this is used to send messages into AMI.

 1package com.f1.ami.relay.fh;
 2
 3import com.f1.ami.relay.AmiRelayIn;
 4import com.f1.ami.relay.AmiRelayOut;
 5import com.f1.utils.PropertyController;
 6
 7//Represents a single instance of an AMI Relay Feed Handler
 8public interface AmiFH extends AmiRelayOut {
 9
10       public static int STATUS_STARTED = 1;
11       public static int STATUS_STOPPED = 2;
12       public static int STATUS_FAILED = 3;
13       public static int STATUS_STARTING = 4;
14       public static int STATUS_STOPPING = 5;
15       public static int STATUS_START_FAILED = 6;
16       public static int STATUS_STOP_FAILED = 7;
17
18       public static final String PCE_STATUS_CHANGED = "STATUS_CHANGED";
19
20       //called during AMI startup
21       //  id - unique id per relay/runtime
22       //  name - name of the relay, ex: my_feedhandler_name
23       //  sysProps - all properties inside ami relay
24       //  props - properties specified to this feed handler
25       //  endpoint - the endpoint for sending messages into AMI. You should hold onto this and call methods on it as messages stream in, etc.
26       public void init(int id, String name, PropertyController sysProps, PropertyController props, AmiRelayIn endpoint);
27
28       //called when this feedhandler is started/stopped. Typically, start is called immediately after all successful init has been called
29       public void start();
30       public void stop();
31
32       //Return the status of this feedhandler (see constants) the string version is for the convenience of users to see why the current status is what it is.
33       public int getStatus();
34       public String getStatusReason();
35
36       //the AppId (aka loginId) associated with this connection. See Reserved Columns, column P for details)
37       public String getAppId();
38
39       //time and place and description of the connection, not required but convenient for end users diagnosing connections
40       public long getConnectionTime();
41       public int getRemotePort(); //ex: 1234
42       public String getRemoteIp(); //ex:  myhost
43       public String getDescription();//ex: someprotocol://myhost:1234
44
45       //when durability is enabled this will be called back when the message has been successfully persisted (see ami.relay.guaranteed.messaging.enabled)
46       public void onAck(long seqnum);
47
48       //E (execute command) See real-time messaging api for details. Note that implementation is optional. If there was an unexpected error, write the details to errorSink. The server is for advanced internal use
49       public void call(AmiRelayServer server, AmiRelayRunAmiCommandRequest action, StringBuilder errorSink);
50}

AmiRelayIn Interface

This is implemented by AMI and is how a feed handler (AmiFH) instance actually communicates with AMI. For example, if a feed handler wants to insert a new row into AMI, it could call the onObject(...) method. Each AmiRelay will be assigned its own personal AmiRelayIn

 1package com.f1.ami.relay;
 2
 3import java.util.Set;
 4import java.util.concurrent.ThreadFactory;
 5
 6import com.f1.ami.relay.fh.AmiFH;
 7import com.f1.ami.relay.plugins.AmiRelayInvokablePlugin;
 8import com.f1.container.ContainerTools;
 9
10//Please note, it's assumed the user is very familiar with the AMI Real-time Messaging API
11//
12//For the encodedMap(s) parameters:
13//
14//  (A) you can use the com.f1.ami.relay.AmiRelayMapToBytesConverter::toBytes(...) method to conveniently convert a map of params to the expected byte protocol.
15//  (B) you can implement the following protocol directly, see protocol at bottom of interface definition
16
17public interface AmiRelayIn {
18
19       public static int RESPONSE_STATUS_OK = 0;
20       public static int RESPONSE_STATUS_NOT_FOUND = 1;
21       public static int RESPONSE_STATUS_ERROR = 2;
22
23       //S (status) message
24       public void onStatus(byte[] encodedMap);
25
26       //R (response to execute command) message.
27       public void onResponse(String I_uniqueId, int S_status, String M_message, String X_executeAmiScript, byte[][] encodedMaps);
28
29       //X (exit) message.  Clean=true means it was an expected exit, false is unexpected, ex line dropped
30       public void onLogout(byte[] encodedMap, boolean clean);
31
32       //L (login) message
33       public void onLogin(String O_options, String PL_plugin, byte[] encodedMap);
34
35       //C (command definition) message
36       public void onCommandDef(String I_id, String N_name, int L_level, String W_whereRowLevel, String T_wherePanelLevel, String H_help, String A_formDefinition,
37                     String X_executeAmiScript, int P_priority, String E_enabled, String S_style, String M_multipleSelectMode, String F_fields, byte[] encodedMap, int callbacksMask);
38
39       //O (object) message for batching. Set seqNum=-1 for no seqNum. All arrays should have same number of arguments.
40       public void onObjects(long seqNum, String[] I_ids, String[] T_types, long E_expiresOn, byte[][] encodedMaps);
41
42       //O (object) message. Set seqNum=-1 for no seqNum
43       public void onObject(long seqNum, String I_id, String T_type, long E_expiresOn, byte[] encodedMap);
44
45       //D (delete) message. All arrays should have same number of arguments.
46       public void onObjectDelete(long origSeqnum, String[] I_ids, String T_type, byte[][] encodedMaps);
47
48       //when the connection is established, this should be supplied.  The optional encoded map will show as parameters on this connection
49       public void onConnection(byte[] encodedMap);
50
51       //when the connection has an unexpected error, this should be supplied.  The optional encoded map will show as parameters on this connection.  error is user-readable a message
52       public void onError(byte[] encodedMap, CharSequence error);
53
54       //Tools for this AMI instance, internal use
55       public ContainerTools getTools();
56
57       //For creating additional thread
58       public ThreadFactory getThreadFactory();
59
60       //Advanced feature: To start another (typically sub) feed handler.
61       public void initAndStartFH(AmiFH fh2, String string);
62
63       //Advanced feature: get invokable plugins (see ami.relay.invokables property)
64       public AmiRelayInvokablePlugin getInvokable(String typ);
65       public Set<String> getInvokableTypes();
66}

//  EncodedMap Protocol:

//

//   TotalMessage:  <-- this is what should be passed in as encodedMaps

//       KeyValuePairCount (signed short) <--number of entries in this map

//       Key[]  <- back to back entry of all key names.  (see below)

//       Value[] <- back to back entry of all values, note there is a different protocol depending on type

//

//   Key:

//            StringLengthOfKey (signed byte)

//      Ascii Representation of key's chars (byte array)

//

//   Value for Int between 0x80 ... 0x7f:

//      0x0A value (byte)

//

//   Value for Int between  0x8000 ... 0x7fff:

//      0x0B value (2 bytes)

//

//   Value for Int between  0x800000 ... 0x7fffff:

//      0x0C value (3 bytes)

//

//   Value for Int between  0x80000000 ... 0x7fffffff:

//      0x0D value (4 bytes)

//

//   Value for Long between 0x80 ... 0x7f:

//      0x0E value (byte)

//

//   Value for Long between  0x8000 ... 0x7fff:

//      0x0F value (2 bytes)

//

//   Value for Long between  0x800000 ... 0x7fffff:

//      0x10 value (3 bytes)

//

//   Value for Long between  0x80000000 ... 0x7fffffff:

//      0x11value (4 bytes)

//    

//   Value for Long between  0x8000000000 ... 0x7fffffffff:

//      0x12 value (5 bytes)

//    

//   Value for Long between  0x800000000000 ... 0x7fffffffffff:

//      0x13 value (6 bytes)

//    

//   Value for Long between  0x80000000000000 ... 0x7fffffffffffff:

//      0x14 value (7 bytes)

//    

//   Value for Long between  0x8000000000000000 ... 0x7fffffffffffffff:

//      0x15 value (8 bytes)

//    

//   Value for Double:

//      0x06 value (8 bytes) (see Double.doubleToLongBits)

//    

//   Value for Float:

//      0x05 value (4 bytes) (see Float.floatToIntBits)

//    

//   Value for Character:

//      0x1A value (2 bytes)

//    

//   Value for Boolean True:

//      0x02 0x01

//   Value for Boolean False:

//      0x02 0x00

//    

//   Value for string <=127 chars in length and simple ASCII (all chars between 0 ... 127)

//      0x09

//      number_of_chars (byte)

//      bytes of string (1 byte per char)

//

//   Value for string >127 chars in length and simple ASCII (all chars between 0 ... 127)

//      0x08

//      number_of_chars (int)

//      bytes of string (1 byte per char)

//    

//   Value for string with extended ASCII (at least one char not between 0 ... 127)

//      0x07

//      number_of_chars (int)

//      chars of string (2 bytes per char)

//    

//   Value for UTC

//      0x1E

//      milliseconds since epoch (6 bytes)

//    

//   Value for NANO timestamp

//      0x1F

//      nanoseconds since epoch (8 bytes)

//    

//   Value for binary data (aka byte array)

//      0x28

//      number_of_bytes (4 bytes)

//      bytes (1 byte per byte of raw data)

CustomRelayFeeder.jpg

Custom Relay Plugin

Overview

The AMI Relay Plugin allows for custom processing of the Realtime Messaging API.

Depending on the return value of processData, the each message is handled as such:

Return Value AMI Behavior
ERROR The errorSink message gets printed, and the incoming message is not processed
NA The mutableRawData value is ignored and the original message is processed by AMI
SKIP The incoming message is not processed
OKAY The mutableRawData value is processed by AMI

Usage

To use the custom relay plugin, place the exported jar file inside the AMI libs directory. During login to the realtime port, a user can specify a custom relay plugin to use with the syntax:

L|I="demo"|P="fully.qualified.relay.plugin.name"

AmiRelayPlugin Interface

 1package com.f1.ami.relay;
 2
 3import com.f1.ami.relay.fh.AmiFH;
 4import com.f1.utils.ByteArray;
 5import com.f1.utils.PropertyController;
 6
 7public interface AmiRelayPlugin {
 8    int ERROR = 1;
 9    int NA = 2;
10    int OKAY = 3;
11    int SKIP = 4;
12
13    int processData(ByteArray mutableRawData, StringBuilder errorSink);
14    boolean init(PropertyController properties, AmiFH fh, String switches, StringBuilder errorSink);
15}

Example

 1package com.f1;
 2
 3import java.util.Map;
 4import java.util.Map.Entry;
 5
 6import com.f1.ami.relay.AmiRelayPlugin;
 7import com.f1.ami.relay.fh.AmiFH;
 8import com.f1.utils.AH;
 9import com.f1.utils.ByteArray;
10import com.f1.utils.FastByteArrayDataOutputStream;
11import com.f1.utils.PropertyController;
12import com.f1.utils.SH;
13
14public class SampleRelayPlugin implements AmiRelayPlugin {
15	private static final String AMI_SAMPLEPLUGIN_PROP = "ami.sampleplugin.prop";
16	private byte[] prefix = "SAMPLE|".getBytes();
17	private final FastByteArrayDataOutputStream buf = new FastByteArrayDataOutputStream();
18	private String prop;
19
20	@Override
21	public boolean init(PropertyController properties, AmiFH fh, String switches, StringBuilder errorSink) {
22		prop = properties.getOptional(AMI_SAMPLEPLUGIN_PROP, "|");
23		if (SH.is(switches)) {
24			Map<String, String> switchMap = SH.splitToMap(',', '=', '\\', switches);
25			for (Entry<String, String> e : switchMap.entrySet()) {
26				final String key = e.getKey();
27				final String value = e.getValue();
28				if (AMI_SAMPLEPLUGIN_PROP.equals(key))
29					prop = (String) value;
30				else {
31					errorSink.append("unknown switch: ").append(key);
32					return false;
33				}
34			}
35		}
36		return true;
37	}
38	
39	@Override
40	public int processData(ByteArray mutableRawData, StringBuilder errorSink) {
41		byte[] data = mutableRawData.getData();
42		if (!AH.startsWith(data, prefix, mutableRawData.getStart())) 
43			return NA;
44		//Process input data
45		int pos = mutableRawData.getStart() + prefix.length - 1;
46		int length = mutableRawData.getEnd();
47		if (pos >= length) {
48			errorSink.append("No data to parse");
49			return ERROR;
50		}
51		//Handle complex parsing here...
52		buf.reset(2048);
53		buf.writeBytes("O|T=\"Table\"|val=100");
54		mutableRawData.reset(buf.getBuffer(), 0, buf.getCount());
55		return OKAY;
56	}
57}

Property Decrypter

This plugin is used to avoid storing plain text passwords inside properties files. Use the Decrypter interface to implement a customized methodology for decrypting/retrieving data. After creating a class that implements the com.f1.utils.encrypt.Decrypter interface, add the full class name to the -Df1.properties.decrypters property. As a result, where ever the ${CIPHER:xxx} syntax within .properties files is encountered the xxx will be passed to the decryptString(...) method. If the decryptString is unable to process the xxx, then simply return null or throw an exception.

Note: Instead of using the Decrypter plugin, you can instead use the tools.sh in conjunction with the -Df1.properties.secret.key.files to encrypt tokens manually.

1package com.f1.ami.relay;
2
3package com.f1.utils.encrypt;
4public interface Decrypter {
5  String decryptString(String encrypted);
6  byte[]decrypt(String encrypted);
7}

Example

 1package com.example;
 2import com.f1.utils.encrypt.Decrypter;
 3
 4//Add to your java vm arguments: -Df1.properties.decrypters=com.example.MyDecrypter
 5public MyDecrypter implements Decrypter {
 6
 7  //Normally this would be more sophisticated, like reaching out to a secure vault, etc.
 8  public String decryptString(String encrypted){
 9    if("secretPass".equals(encrypted))
10      return "password123";
11    return null;
12  }
13  public  byte[] decrypt(String encrypted){
14    String s=decryptString(encrypted);
15    return s==null ? null : s.getBytes();
16  }
17}
18
19// Now, if you put this inside your properties, some.password will be set to password123:
20//   some.password=${CIPHER:secretPass}