Custom servlet plug-ins are JEE servlets that can be deployed as plug-ins within your Appian instance. A working understanding of how JEE servlets work is required for creating this type of plug-in.
When you define a servlet plugin you will have the option of making it stateless or not.
Any servlet that maps to the URL pattern /plugins/servlet/stateless/*
will be a stateless servlet and will use HTTP Basic Authentication on each request to authenticate each request and should not access or create a JEE session.
All servlets that match /plugins/servlet/*
but do not match /plugins/servlet/stateless/*
are not stateless and require the user to have an authenticated session in order to access them, but do have access to a JEE session.
The following principles must be followed:
/plugins/servlet/stateless
pattern must not require or create a session.This example assumes you are using Eclipse as your IDE.
Create a new Java project in Eclipse.
Configure the Java Build Path.
<APPIAN_HOME>/_admin/sdk/appian-plug-in-sdk.jar
- Your plug-in must be designed to access only the classes and methods documented in the Public API javadocs.javax.servlet
package to the build path. This is typically in the Java EE library provided with your application server.ImportExportService
), you'll also need to add the library that contains the javax.inject
package.Configure Project Folders.
src
folder and select New > Folder.META-INF
in the folder name field and click Finish.META-INF
folder selected, right-click and select New > Folder. Type lib
in the folder name field. Click Finish.src
folder selected, right-click and select New > Package. Type your desired package structure in the name field. For example: com.example.plugins.<YOUR_PLUGIN>
Click Finish. Your file structure should appear similar to the following diagram.
1
2
3
4
5
|_ src
|_ com.example.plugins.<YOUR_PLUGIN>
|
|_ META-INF
|_ lib
src/META-INF/lib/
folder.
src/META-INF/lib/
folder.Create your class and add it to the src/com.example.plugins.<YOUR_PLUGIN>
package.
com.example.pluginname
).com.appiancorp.suiteapi.servlet.AppianServlet
.See below: Java Component
When calling Appian's Java API, only use the Public API. Generally public interfaces are found in com.appiancorp.suiteapi
.
See also: Java API
Update your Java Build path to include any new JAR files; otherwise Eclipse won't compile.
Register the servlet in an appian-plugin.xml
file.
See below: Configuring the appian-plugin.xml File
Add your appian-plugin.xml file to the src
folder at the root level.
Export your project as a JAR file.
<APPIAN_HOME>/_admin/plugins
).
All plug-ins must contain an appian-plugin.xml
configuration file. Plug-ins that do not contain this configuration file won't be registered in Appian.
1
2
3
4
5
6
7
8
9
10
11
<appian-plugin ...>
<plugin-info>...</plugin-info>
<servlet name="Example Servlet" key="exampleServlet" class="com.example.servlet.ExampleServlet">
<description>An example servlet</description>
<url-pattern>/example/- </url-pattern>
<init-param>
<param-name>foo</param-name>
<param-value>bar</param-value>
</init-param>
</servlet>
</appian-plugin>
appian-plugin: The main parent element. This element defines the plug-in properties and references. The name
is used for documentation purposes only. The key
must be unique among all Appian plug-ins. It represents a unique namespace for your plug-in function. We recommend using the same convention established for Java package names.
plugin-info: This element contains plug-in metadata, including the following subelements.
servlet: Defines the servlet module that will be deployed.
com.appiancorp.suiteapi.servlet.AppianServlet
.description: subelement of servlet (optional) Servlet description.
url-pattern: subelement of servlet Pattern of the URL to match. Multiple url-patterns can be defined. *
and ?
wildcards are valid.
init-param: subelement of servlet (optional) Init parameters for the servlet. Multiple init-param elements can be specified for multiple.
com.appiancorp.suiteapi.servlet.AppianServlet
.init()
method of the servlet is not called on application server startup nor plug-in deployment. It's called the first time the servlet is accessed.Once deployed, the servlet will be invoked when requests match the URL pattern relative to <context>/plugins/servlet/
. For the servlet defined in the example appian-plugin.xml
above, the path would be the following:
1
<host:port>/suite/plugins/servlet/example/-
By default, a custom servlet plug-in is not available to unauthenticated users. However, access can be modified depending on your needs.
Authenticated Access: This is the default setting for a custom servlet plug-in. If the servlet is not listed in the Spring Security Unsecured List, a request for a custom servlet URL that does not have a current session will redirect the user to the login page.
The <context>/plugins/servlet/*
pattern is authorized for authenticated users within any role (Application Users and Designers).
Unauthenticated Access: You can allow unauthenticated access to your servlet by adding your custom servlet plug-in to the Spring Security Unsecured List.
<APPIAN_HOME>/deployment/web.war/WEB-INF/conf/security/spring-security-02-unsecured.xml
and name it spring-security-02-unsecured-override.xml
.spring-security-02-unsecured-override.xml
, add an entry such as <sec:http pattern="/plugins/servlet/<your servlet url pattern>" security="none"/>
.Because this type of access falls outside of Appian's security perimeter, implementation of this modification must be tested for security independently.
Note: Unauthenticated access to content is prohibited in all Appian Cloud environments.
This example is a simple servlet that returns an array of JSON objects with a relevant subset of fields for Appian process tasks assigned to the currently authenticated user.
This example assumes the following scenario:
Notes:
org.json
package is used to create the JSON representation of the task. This library does not need to be included separately because it is already provided by the plug-in container.batchSize
if only startIndex
is given, but will use the defaults for both startIndex
and batchSize
if only batchSize
is given or if neither parameter is given. Adapt the example as you see fit.Alternatives:
If the "mashup" scenario is not appropriate to your use case, but instead a back-end server-to-server integration matches your use case better, this example can be adapted to fit that example in the following ways:
url-pattern
element in the appian-plugin.xml
to <url-pattern>/stateless/tasklist</url-pattern>
to make the servlet a stateless servlet. This will cause the servlet to no longer require an authenticated session and instead will protect it using HTTP Basic authentication.ProcessAnalyticsService.getMyTasks
.Java Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.appian.example.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
import org.json.JSONObject;
import com.appiancorp.services.ServiceContext;
import com.appiancorp.services.WebServiceContextFactory;
import com.appiancorp.suiteapi.common.Constants;
import com.appiancorp.suiteapi.common.ResultPage;
import com.appiancorp.suiteapi.common.ServiceLocator;
import com.appiancorp.suiteapi.process.TaskSummary;
import com.appiancorp.suiteapi.process.analytics2.ProcessAnalyticsService;
import com.appiancorp.suiteapi.process.exceptions.ResultPageSizeException;
import com.appiancorp.suiteapi.servlet.AppianServlet;
public class TaskList extends AppianServlet {
private static final String CHARACTER_ENCODING = "UTF-8";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String TEMPO_TASK_URL_BASE = "/tempo/tasks/task/";
private static final String START_INDEX_PARAM = "startIndex";
private static final String BATCH_SIZE_PARAM = "batchSize";
private static final int DEFAULT_START_INDEX = 0;
private static final int DEFAULT_BATCH_SIZE = 20;
private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private String baseUri;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
baseUri = extractBaseUriFromRequest(req);
String startIdxParam = req.getParameter(START_INDEX_PARAM);
String batchSizeParam = req.getParameter(BATCH_SIZE_PARAM);
int startIndex = DEFAULT_START_INDEX;
int batchSize = DEFAULT_BATCH_SIZE;
try {
startIndex = Integer.parseInt(startIdxParam);
batchSize = Integer.parseInt(batchSizeParam);
} catch (NumberFormatException e) {
// do nothing, the defaults will be used
}
ServiceContext context = WebServiceContextFactory.getServiceContext(req);
ProcessAnalyticsService pas = ServiceLocator.getProcessAnalyticsService2(context);
ResultPage taskResultPage = new ResultPage();
try {
taskResultPage = pas.getMyTasks(
startIndex,
batchSize,
TaskSummary.SORT_BY_ASSIGNED_TIME,
Constants.SORT_ORDER_DESCENDING);
} catch (ResultPageSizeException rpse) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rpse.getMessage());
}
JSONArray tasksJson = new JSONArray();
for (Object result : taskResultPage.getResults()) {
TaskSummary task = (TaskSummary)result;
tasksJson.put(convertToJson(task));
}
resp.setContentType(JSON_CONTENT_TYPE);
resp.setCharacterEncoding(CHARACTER_ENCODING);
PrintWriter out = resp.getWriter();
out.write(tasksJson.toString());
out.flush();
out.close();
}
private String extractBaseUriFromRequest(HttpServletRequest req) {
String scheme = req.getScheme();
int port = req.getServerPort();
StringBuffer url = new StringBuffer(42);
url.append(scheme);
url.append("://");
url.append(req.getServerName());
if ((scheme.equalsIgnoreCase("http") && port != 80) ||
(scheme.equalsIgnoreCase("https") && port != 443)) {
url.append(":");
url.append(port);
}
url.append("/");
url.append(req.getContextPath());
return url.toString();
}
private JSONObject convertToJson(TaskSummary task) {
Timestamp assignedTime = task.getAssignedTime();
Timestamp deadline = task.getTaskDeadline();
Object assignedTimeJson = assignedTime != null ? df.format(assignedTime) : JSONObject.NULL;
Object deadlineJson = deadline != null ? df.format(deadline) : JSONObject.NULL;
return new JSONObject().put("Id", task.getId())
.put("DisplayName", task.getDisplayName())
.put("AssignedTime", assignedTimeJson)
.put("TaskDeadline", deadlineJson)
.put("Link", getLinkForTask(task));
}
private String getLinkForTask(TaskSummary task) {
Long id = task.getId();
return baseUri + TEMPO_TASK_URL_BASE + id;
}
}
Plug-in Configuration File
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<appian-plugin key="com.appian.example.servlet" name="Task List Servlet Plugin">
<plugin-info>
<description>Task List Servlet Plug-in Example</description>
<vendor name="Appian" url="https://forum.appian.com/suite/help/7.6/Custom_ServletPlugins.html" />
<version>1.0</version>
<application-version min="7.1" />
</plugin-info>
<servlet name="Task List Servlet" key="taskList" class="com.appian.example.servlet.TaskList">
<description>
An example servlet that responds to GET requests with a JSON array containing the authenticated user's task list
</description>
<url-pattern>/tasklist</url-pattern>
</servlet>
</appian-plugin>
Servlet Plug-ins