Client Module

The Client module is available for every robot and provides basic interfaces and classes to develop robots. This page discusses the Client module, its interfaces and classes, and robotic process example.

The corresponding Maven dependency is jidoka-client-api-x.y.z.jar, where x.y.z represents the version number.

The most important interfaces and classes, grouped in packages, include:

Package com.novayre.jidoka.client.api:

  • JidokaFactory: utility class that enables the installation of the different modules of Appian RPA. It is used to obtain an instance of IJidokaServer. For the rest of module implementations, though they can be obtained through this class, they are typically obtained directly from static methods of the interfaces of the modules themselves, since they are easier to use.
  • IJidokaRobot: this interface basically enables functionality for keyboard and mouse control, clipboard content management, delays, etc.
  • IJidokaServer: enables functionality for robot's communication with the Appian RPA Server. It is basically used to send information to the console, write in the execution log, report the number of items to be processed, the beginning of an item's processing, the end of an item's processing, its result, etc. It inherits from org.apache.commons.logging.Log, from IJidokaContext and from IJidokaStatistics.
  • IKeyboardSequence: this interface emulates a composition or a sequence of keystrokes just as a human would do. For example, the combination of keys Alt+F4 may be considered a sequence.

Package com.novayre.jidoka.client.api.multios:

  • IClient: interface that represents an abstraction to make low-level OS API calls. This interface is preferred over IWindows, aiming to make development independent of specific Operating Systems. Along with this interface, there are specific interfaces for other OS: Windows, Linux and Mac.

Package com.novayre.jidoka.windows.api:

  • IWindows: interface of the Appian RPA module that represents a Windows operating system, inherits from IJidokaRobot. It enables functionality to handle each window, enable them, minimize them or maximize them, obtain its controls, access and manage processes.

This page will dive deeper into more details about these classes. There are many other classes within the API jidoka-client, described throughout this guide.

IRobot and @Robot

Every Appian RPA Robot must implement the interface IRobot located in the package com.novayre.jidoka.client.api.IRobot, as well as use the annotation @Robot. In our example, a class implementing that interface and annotated with @Robot is a robot, so that one single project can have several robots if we have several classes implementing the interface.

cleanUp

This interface publishes the method cleanUp(), which we have seen in detail earlier.

The Appian RPA platform will always call this method, regardless of the result of the robot's execution, that is, whether it has ended without errors or it has failed throwing some exception. It is a good moment to perform operations such as closing the applications that the robot has opened or, above all, sending the files that have been generated during the execution to the server.

1
2
3
4
@Override
public String[] cleanUp() throws Exception {
    return null;
}

To send the files to the server, the String[] returned by the method must contain the local paths to the generated files.

startUp

Likewise, the method startUp() is called at the beginning of the execution of a robot, prior to the execution of the actions of the workflow. This way, this method can be useful to verify any condition on the resource, for instance: to know if a certain application is available, the screen resolution or if the robot can be executed, as well as to initialize the different instances needed by the robot, as IJidokaServer or IWindows.

In the code below, we can see how these instances are initialized, in addition to checking if the robot can be executed or not. Only if it is possible, the robot can continue and start with the execution of the workflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean startUp() throws Exception {
    // Initialization of the robot components
    server = JidokaFactory.getServer();
    windows = IJidokaRobot.getInstance(this);
     
    String robotName = server.getExecution(0).getRobotName();
    boolean robotAllowedNow = server.getExecution(0).isRobotAllowedNow(robotName);
     
    server.info(String.format("Is this robot allowed now?: %s", robotAllowedNow));
     
    return robotAllowedNow;
}

beforeGetInstructions

There are several methods that are executed before the robot execution starts. One of them is beforeGetInstructions, this method is invoked before the server gets the instructions from the console.

In the following code we can see how the beforeGetInstructions method is used to calculate the available free space.

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean beforeGetInstructions() throws Exception {
    server = (IJidokaServer< ? >) JidokaFactory.getServer();
          
    File file = new File(server.getCurrentDir());
    long freeSpace = file.getFreeSpace();
  
    server.info(String.format("Free space: %d bytes", freeSpace));
      
    return IRobot.super.beforeGetInstructions();
}

beforeGetSupportFiles

Another method that can be invoked before starting the execution is beforeGetSupportFiles, which will be invoked before obtaining the support files of the robot from the console. This method can be used, for example, to remove previously downloaded support files.

In the following code we can see how this method is used to list the previously downloaded support files.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean beforeGetSupportFiles() throws Exception {
    server = (IJidokaServer< ? >) JidokaFactory.getServer();
          
    File folder = new File(server.getCurrentDir());
    File[] files = folder.listFiles();
          
    for(File file:files) {
        server.info(file.getPath());
    }
          
    return IRobot.super.beforeGetSupportFiles();
}

Error handling

As in any Java program, error handling consists basically of handling exceptions which are thrown during the execution and may cause a change in the execution sequence.

Basically, we can follow two strategies for exceptions: we can catch them and treat them when they occur, or we can raise them up so that they can be treated in the code that called the method where the exception was detected.

By default, a Appian RPA Robot's execution will end if an exception is thrown from the action that is currently running.

Appian RPA allows exception handling centralization in a single point, and most importantly, it allows reallocating the execution in any action of the workflow defined for the robot. This way, we achieve two things: the first one is clustering the code that handles the exceptions; the second and most important one is allowing the robot to continue processing items from any point of the workflow immediately after treating the exception detected, without needing to build conditional blocks in the workflow for each possible error that may happen.

All these advantages are obtained by simply overwriting the method manageException from the interface IRobot.

1
2
3
4
5
@Override
public String manageException(String action, Exception exception)
    throws Exception {
    ...
}

Appian RPA will invoke this method whenever an action throws an exception without catching it, passing the method name associated to the action and the exception as parameters, encapsulated in a InvocationTargetException. Therefore, to retrieve the original exception we will use the method getCause from Exception. The method must return the name of method associated with the action to which the process will be redirected once the exception handling finishes, or it will throw an exception to cause the end of the robot's execution. The default method's implementation throws the exception received as a parameter, without processing it, which causes the corresponding end of the robot's execution. It is recommended to call this default implementation at the end of the method manageException as a fallback, to prevent the loss of exceptions that have not been caught due to having overwritten the method.

Even though this method ends throwing an exception, and hence, indicating that we want our robot to end, the methods cleanUp from the workflow libraries used and the robot itself, will be called anyway.

When we manage exceptions globally, we must pay attention to one special case, related to using the same method by more than one action in the workflow. Under these circumstances, the action that should continue the execution of the workflow would be undetermined. To avoid this, since we cannot make any supposition about it, we should encapsulate the code in a method, that will be called from the methods associated with the actions involved. This way, we can have several actions, each calling different methods, but finally executing the same piece of code.

In short, when you use global error handling you should avoid using the same method in different actions, to prevent the robot from resuming on an undetermined point of the workflow.

Every Appian RPA workflow library must implement the interface INano and use the annotation @Nano at class level. In our projects, if a class implements this interface, then it is a nanorobot. Hence, a single project can have several nanorobots.

1
2
3
4
@Nano
public class NanoRobot implements INano {
    ...
}

This interface publishes the methods cleanUp() and init(). The Appian RPA platform will always call these methods to initiate the nanorobot (before any other action), and to end the execution of the robot that is using the Appian RPA workflow library.

The annotation @FieldLink is used in the libraries to share values between the Appian RPA libraries and the robots. This annotation allows automatically sharing the value associated with the annotation's attribute between a robot and a Appian RPA library. The annotation receives one parameter, which defines the value name in the Appian RPA context.

When you use the annotation in a robotic process, you must specify the name of the library followed by "::", and the name of the attribute. If you use the annotation within the library, you must only specify the name of the attribute.

We can also define the annotation within the robot using "::" followed by the name of the attribute, which makes the value global, and so it can be used in any Appian RPA library.

Below we can see the definition of an attribute that is published globally in the Appian RPA platform.

1
2
3
4
5
/**
 * Server instance.
 */
@FieldLink("::server")
private IJidokaServer<?> server;

In the following code snippet, we can see how a nanorobot's attribute is defined. Below it, another snippet shows how that attribute would be used within a robot.

1
2
3
4
5
/**
 * Application directory location.
 */
@FieldLink("applicationDirectory")
private String applicationDirectory;
1
2
3
4
5
/**
 * Application directory location.
 */
@FieldLink("NanoRobot::applicationDirectory")
private String applicationDirectory;

The above-mentioned literal "::" can be modified indicating the number of the library version to be used between the two colons. If you don't indicate the version, you are saying that any version of the library with the specified name will receive the attribute.

Further on, we will see how you can interact with the robots from the Appian RPA API in the IJidokaContext<T>, but now we are going to make a brief reference to the use of the Appian RPA API to see how the literal ":" works.

When you call a library method from the Appian RPA API using the method callNano (that will be seen later), you use a naming that is like the one specified for @FieldLink. In the case of methods, we can indicate the name of the library followed by ":" and the name of the method, or we can indicate the name of the library followed by ":", the number of the library version followed by ":", and the name of the method.

JidokaFactory

This class allows you to obtain an instance of an Appian RPA module. If any error occurs, the returned value will be null.

To get an instance of a particular class, you must use the method: getInstance(IRobot robot, String className); where className is the name of the class to get.

Additionally, you can get a reference of IJidokaServer (seen later) by calling the method getServer().

IJidokaServer

An object implementing this interface will do the communications with the Appian RPA Server and, thus, with the console. In addition to sending log messages to the console, there are three important things this object must do to make an optimal monitoring of a robot from the console:

  • Notify the server about the number of items to be processed: as mentioned earlier, our robot's operation is based on items. Earlier in this guide we have talked very briefly about item's meaning in the Appian RPA terminology. We must identify the item during our robot's development. For example, a robot whose mission is to check the stock in the warehouse from a list of products should: read each input product (INPUT), connect to the stock database and check the stock available for the product in process, and save this information somehow (OUTPUT). In this case, the item is the product, that is, each product is an item to be processed. Notifying the total number of items to be processed is essential for the console to be able to monitor the robot's operation, evaluate each item's processing time, on which step it is, estimate the total processing time, etc. Later, we will see how to send this information to the Appian RPA Server.
  • Notify the server of the time when an item processing starts: We must inform the server about when an item processing begins. The information needed by the server is:
    1. The order of the processing item, that is, its place on the list of items.
    2. An identifier, for example "Item 1" or "Product 5". This identifier may be any text string, though common sense tells us it should properly represent the item, since this information will be shown in the console, within the robot's execution log, and so it will help us identify the item on which the operation was performed.
  • Notify the server of the time when an item processing ends and its result: This way, the console will know the time spent on processing that item, and the result of that process.

All these operations make the statistics system work properly. Statistics may be important; therefore, an appropriate use of this API is mandatory.

The IJidokaServer interface can use generics, IJidokaServer<T extends Serializable>, although this is optional. The specified parameter is a class implementing Serializable. In Appian RPA, the generic class associated to IJidokaServer is called context class.

We know that IJidokaServer<?> inherits from IJidokaStatistics, IJidokaContext<T> and Log. In the following points we will see these interfaces in more detail.

IJidokaServer methods

Methods directly present in the IJidokaServer interface override those in the Log interface (explained further) of apache-commons, besides the activeThirdPartyLog method.

This method allows to send third-party log to the execution trace by using the org.apache.commons.logging.Log interface. Be aware that this way of sending log to the trace will work if the classes sending it are not inside com.novayre.jidoka package.

IJidokaStatistics

IJidokaStatistics allows you to notify the server about the number of items to be processed, the beginning of the item processing and its result.

By notifying the result of an item processing, it is considered to be finished.

The methods of IJidokaStatistics that enable this are:

  • void setNumberOfItems(int numberOfItems): this method informs the server about the total number of items to be processed.
  • void setCurrentItem(int itemIndex, String itemKey): this method informs about the beginning of an item processing, including the item position (1-based) within the list of items and an item description.
  • The methods for notifying the result of an item processing are:
    • void setCurrentItemResultToOK()
    • void setCurrentItemResultToWarn()
    • void setCurrentItemResultToFail()
    • void setCurrentItemResultToIgnore()

Respectively, they inform about a result with no error, a process with some warning, a failed process or an ignored item. It's a developer's decision to choose what kind of result to send to the server. These methods are overloaded and allow to send a description of the processing result. This description is shown in the execution log.

  • The methods for notifying the result of an item processing, including a description, are:
    • void setCurrentItemResultToOK(String detail)
    • void setCurrentItemResultToWarn(String detail)
    • void setCurrentItemResultToFail(String detail)
    • void setCurrentItemResultToIgnore(String detail)

Where detail is the complementary description that will be shown in the execution log.

  • These methods have been overloaded to allow you to include information about statistics per item. This is aimed to be able to provide additional information about the result of an item processing. For example, we could add a parameter to include the cause of a warning. The overloaded methods are as follows:
    • void setCurrentItemResultToOK(String detail, Map<String, String>)
    • void setCurrentItemResultToWarn(String detail, Map<String, String>)
    • void setCurrentItemResultToFail(String detail, Map<String, String>)
    • void setCurrentItemResultToIgnore(String detail, Map<String, String>)

We will add the statistics per item within the map that is used as the second parameter. This could be an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Set the result of the process of the current item to OK.
 */
private void itemOk() {
        
    Map<String, String> map = new TreeMap<>();
    map.put(ETestCaseProperty.RESULT.name(), EResult.OK.name());
    map.put(ETestCaseProperty.DETAIL.name(),
            ETestCasePropertyDetail.PASSED.name());
    
    Enum<?> currenTestCase = tests.get(currentItem - 1);
    
    server.setCurrentItemResultToOK(currenTestCase.name(),
            map);
    
    currentItem++;
}

As you can see here, we are using a Map in which the keys RESULT and DETAIL and the values OK and PASSED are added. This way, for each item we can inform about the properties defined in the Map.

  • Previous methods can be emulated with setCurrentItemResult(ItemData item), which, additionally, extends the possibilities for notifying the server about results of an item.

The support class ItemData is used to specify different options:

  • index(int i): indicates the index corresponding to the item.
  • key(String k): the item key.
  • result(EResult r): the item result, which can be OK or WARN.
  • subResult(ESubResult sr): subresult for the item, which, in addition, is a categorization of the result.
  • detail(String d): detail for the item result.
  • properties(Map<String, String> p): properties for the item.
  • void setDumpSection(String dumpSection): This method allows you to send a text that will be part of the notifications of the event ROBOT_END of the console. The text support HTML format.

We stress that in the Appian RPA robots' development, it is very important to notify the server about these three key aspects:

  • Total number of items to be processed.
  • The beginning of an item processing.
  • The process result, with or without description, including the index of the item whose processing has ended.

IJidokaContext<T>

The context class enables you to save persistent information between executions, that is, the status or the value of the context class is kept from one execution to another. This is justified, for example, for those cases in which, by any reason, we are expecting that our robot won't be able to process all the items. Sometimes, the list of items is too large and the robot may need hours to process them all.

In such cases, robot's operation may be interrupted by different reasons (the applications it is using are not prepared for long processing times, machine performance, etc.). Through the context class, for future executions we can make the robot resume its operation on the item where it was interrupted, instead of starting again from the first item, which could lead to the same situation as long as the conditions that caused the interruption don't change. This would lead, inevitably, to never completely processing the list of items.

If the list of items to be processed is obtained from an Excel file or any other input provided by the user, the context class may not be necessary, because the tracking of what has been processed and what has not yet, may be registered in the Excel or the specified input itself, thus being an output of the process, used in turn as input for the next execution.

Sometimes, the list of items to be processed is not known when the process starts, because it is dynamically gathered by the robot itself. In such cases, the context class will allow us to know to which point the process has reached.

As mentioned earlier, the context class must be serializable. This is because we will send the class to the server to be stored, to be reused in a later execution.

Let's remember that a serializable class is every class implementing the interface Serializable, and whose members are also Serializable. Typically, within the context class we will store values that allow our robot to resume its operation at the point it was interrupted.

Suppose a robot must email the purchase invoice in PDF format to a list of customers of a shop. This list is stored in an Excel file and it contains basically the name, ID card, email and details of the purchase made. Each row in the Excel file contains one purchase, so that there may be more than one row per client and not necessarily consecutive, depending on the number of purchases and the moment when a customer has made them. Imagine the Excel contains 1000 rows. Our robot should read the Excel file, starting at the first row, retrieve the name, the email address, the ID Card and the purchase data, generate the PDF with the invoice, and send it by email.

If, for some reason, our robot's operation is interrupted (a system error, power failure, etc.), the next time we launch our robot it will have to start again, which will mean a lot of time wasted, assuming a long processing time per row. Moreover, it doesn't seem a good idea to send repeated emails to customers who have already received their invoices.

We can avoid this in two ways.

The first way consists of writing on the input Excel file the result of each item processed (each invoice) and using the input Excel file as an output Excel file, which in turn would be the input for the next execution.

The other way consists of creating a context class called, for example, ClientsContext, which would have a property called lastRowProcessed that will store the number of the last row of the Excel file that has been processed correctly. Our class would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.Serializable;
public class ClientsContext implements Serializable {

    private static final long serialVersionUID = 1L;
    private int lastRowProcessed = 0;

    public int getLastRowProcessed() {
        return lastRowProcessed;
    }
    
    public void setLastRowProcessed(int lastRowProcessed) {
        this. lastRowProcessed = lastRowProcessed;
    }
}

In our example, the object server would be declared as follows:

1
2
private IJidokaServer<ClientsContext> server;
server = (IJidokaServer<ClientsContext>) JidokaFactory.getServer();

Later, when the item is processed, we will send an instance of ClientsContext to the server to create the persistent context. The sentences that retrieve and set the context class are:

  • server.getPersistentContextValue(), which retrieves the context class. If the context class did not exist, we would receive null.
  • server.setPersistentContextValue(clientsContext), which sets or updates the context class (clientsContext is an instance of ClientsContext).
  • server.resetPersistentContextValue(), which resets (removes) the stored context class.

It is important to note that context is only read once at the beginning of the execution. This way, any change made on it will not be reflected until next execution. This is so because it was meant for that purpose, to get it at the start and update it at the end. Additionally, a good practice, as said before, is to set the context class every time an item is correctly processed.

In our example, this way we can always know which row has been the last to be successfully processed:

1
2
clientsContext.setLastRowProcessed(row);
server.setPersistentContextValue(clientsContext);

Here row is the position of the last processed row.

But, what shall we do with the context class when all rows have been processed? In such case, it is a good practice to send an empty context class:

1
server.setPersistentContextValue(new ClientsContext());

Then, when a new execution starts, we would obtain that empty class as the context class, thus managing the case of receiving null:

1
2
3
4
5
clientsContext = server.getPersistentContextValue();

if(clientsContext == null) {
    clientsContext = new ClientsContext();
}

Going into more technical details, another option for more complex context classes is to send a String as a class, whose value is a JSON obtained through the serialization of an object. Appian RPA will store the String and it's up to us to restore (deserialize) the object once retrieved through server.getPersistentContextValue().

IJidokaContext methods

In this section, we will list the methods provided by the interface IJidokaContext<T> to IJidokaServer<?>.

This interface allows you to manage the context between robot executions as well as obtaining parameters.

  • Map<String, String> getParameters(): this method returns the robot execution parameters. When we run a Appian RPA robotic process from the console we can include a value for the parameters (instructions as seen in the console) that we have previously created on the robot setup window. These parameters can be required or optional parameters. In case the parameter was created as required, we should have specified a default value.

Suppose we are developing a robot to get a list of books from a bookstore's web page. We can create a parameter called "author", whose default value could be "**". This parameter will allow us to filter the books by author, so that if our robot receives the parameter with the value "**", it will retrieve all the books, but if it receives any other value, for example "Carl Sagan", it will only retrieve the books whose author is Carl Sagan.

We can receive the parameter this way:

Suppose you're developing a robotic process to get a list of books from a bookstore's web page. Create a parameter called "author", whose default value could be **. This parameter will allow you to filter the books by author, so that if your robotic process receives the parameter with the value **, it will retrieve all the books. If it receives any other value, for example Carl Sagan, it will only retrieve the books whose author is Carl Sagan.

1
String paramAuthor = server.getParameters().get("author");
  • *List getSupportFiles()*: returns the paths of the support files that the robotic process has configured in the console.
  • String getCurrentDir(): returns the robotic process's folder. Every robotic process has its own folder within the client's folder. This page assumes that the client is on C:\AppianRPA, though it can be any other folder. If, for example, the robotic process is called RobotLibrary, this method will return:

  • List<Path> getSupportFiles(): returns the paths of the support files that the robot has configured in the console.

  • String getCurrentDir(): this method returns the robot's folder. We had mentioned earlier that every robot has its own folder within the client's folder. Since the beginning of this guide we have assumed that the client is on C:\AppianRPA (though it can be any other folder). For example, if our robot is called, say, RobotLibrary, this method will return:
1
C:\AppianRPA\AppianRPA-workspace\RobotLibrary

This method is helpful, for example, to find out in what folder we should save the files generated by the robot, which should be in the robot working directory. Obviously, we can also create subfolders to save the files by using the class Paths:

1
2
3
4
5
6
7
// Creating the path for file.txt in the robot local working directory
File file = Paths.get(server.getCurrentDir(), "file.txt").toFile();

// Creating the path for file.txt in a subdirectory of the robot local
// working directory
File file = Paths.get(server.getCurrentDir(), "subdir",   
"file.txt").toFile();
  • void sendScreen(String description): this method takes a screen shot of the machine on which the robot is running and sends it to the server, passing a description in the parameter description.
  • void sendScreenIfCaptureModeEnabled(String description): this method is like the previous one, but it only takes the screen shot if screen capture was enabled from the console when the robot was launched.
  • BufferedImage getScreen(): this method returns the screen shot by using an object BufferedImage (java.awt.image.BufferedImage), so that we can perform some operation on it.
  • BufferedImage getScreen(Rectangle rectangle): this method is like the previous one, but it only captures the area delimited by the rectangle received as a parameter.
  • T getPersistentContextValue():it returns the context class which was saved in previous executions of our robot, or null if there is no previous context class.
  • void setPersistentContextValue(T value): this is also a known method. It allows you to set or update the context class. Since the context class is used to be able to resume a robot execution at the point it was interrupted, it makes little sense to set the context class during the item processing. It is more logical to set it or update it immediately after an item has been processed.
  • void resetPersistentContext(): this method resets (removes) the persistent context for the robot.
  • <C extends Serializable> IJidokaGlobalContext<C> getGlobalContext(): this method returns a global context in which we can save and retrieve data beyond executions scope and may even be shared between different robots
  • Map<String, Object> getFieldLinks(): this method allows you to access the fields that enable communication with the Appian RPA libraries or nanorobots. It returns a Map, whose key will match the field's annotation @FieldLink in the library. With that key, we can set or get its value. The value of the elements in the Map is updated between the different calls to the robot or library actions. Likewise, if we update a value directly on the Map, it will be available for the next execution within the attribute with the associated FieldLink.
1
2
3
4
// Update a value in the map with the shared attributes
server.getFieldLinks()
    .put("Aeb3411BancoPastorLibrary::applicationPassword",
            server.getParameters().get("applicationPassword"));

In case we're using linked attributes, it is not recommended to manage the map programmatically. It will suffice to assign a new value for the attribute.

  • void callNano(String nanoAction): this method allows you to call an action defined in a Appian RPA library. This action does not accept input or output parameters.
1
2
// Call nano-robot with API method to close application
server.callNano("Aeb3411BancoPastorLibrary:closeApplication");
  • Object callNanoObject(String nanoAction, Object... parameters): This method allows you to call an action defined in a Appian RPA library with parameters, and the called method to return the result. The called method must receive an object Object… as a parameter.

  • List<IUsernamePassword> getCredentials(String application): This method allows you to get a list of credentials associated with an application name.

    1
    2
    
    // Get first credentials for Banco Pastor
    IUsernamePassword credentials = server.getCredentials("BANCO_PASTOR").get(0);
    
  • IUsernamePassword getCredential(String application, boolean reserve, ECredentialSearch search): gets a credential associated with an application with the possibility of reserving the credential and specifying the search algorithm, distributed or first listed. By default, the first credential listed will be the one returned.

  • void downloadSupportFile(String relativePath): This method allows you to download support files. The file is identified on the server through the relative path that it receives as a parameter, with respect to the folder in which support files are stored. It will be downloaded in the specified path in the resource, but with respect to the robot's working directory. Although the robots automatically download the resource files at the beginning of the execution, this functionality is very useful to develop robots that use Falcon. It allows you to add and change images during the debugging, helps you through breakpoints to pause the execution, and provides the Display view in Eclipse (for example) to run code under remote debugging. All this without the need of canceling the robot's execution.

  • boolean updateCredential(String application, String username, String password): This method allows you to update the password associated to a specific username and application, therefore they must be previously registered in the platform. It returns true if the update is possible and false otherwise.

One of the situations in which you may find this method useful is when a password is about to expire. In that situation, the robot would be able to update the password, so that in future executions it can still log in.

1
2
3
if (mustUpdate) {
    server.updateCredential("BANCO_PASTOR", "admin", "NEW_PASSOWRD");
}
  • void registerEvent(String eventDetailText): This method allows you to create an event within the platform with a specific associated text. You will be able to watch the event on the specific console section, and it will be of type ROBOT_EVENT.

  • String getPreviousAction(): This method returns the previously executed action. If the current action is the first one, it will return null.

  • String getCurrentAction(): This method returns the current running action.

You can retrieve information related to the current execution and even previous robot's executions.

  • IExecution getExecution(int numberOfOlderExecutionsToGet): This method allows you to get data about the current and the previous robot's executions. The parameter numberOfOlderExecutionsToGet indicates the number of previous executions that will be retrieved: 0 means that only the information about the current execution will be retrieved, -1 means that the data about all the existing executions will be retrieved. This last option should be avoided or used carefully, since it may cause console slowness or overload.

Sometimes, it is necessary to create a robot to be executed very frequently. These executions may be not important, either because they don't process any item or due to a functional criteria. To be able to remove these executions easily, we can call the following method:

  • executionNeedless(String reason): To be invoked to indicate that the current execution must be removed after it ends. The parameter is the reason for the removal, which will appear in the event EXECUTION_NEEDLESS generated at the same time. It is important to note that an execution of this type will always be removed, so only the event EXECUTION_NEEDLESS remains for later queries.

Another important functionality provided by this interface is the access to encryption / decryption methods that can be used to encrypt / decrypt sensitive information before displaying it in places such as the execution log or the functional data of the queue items.

  • String encrypt(String key, String text): Encrypts a text using an 8 characters key.

  • String decrypt(String key, String text): Decrypts an encrypted text using an 8 characters key.

  • String encrypt2(String key, String text): Encrypts a text using a 32 characters key.

  • String decrypt2(String key, String text): Decrypts an encrypted text using a 32 characters key.

IJidokaGlobalContext<T>

The interface exposes the necessary methods to save and retrieve data from the global context, which is a persistent key-value type storage.

You can persist objects of any type only if they are serializable.

The global context is shared between every robot in the console, regardless of the permissions they have assigned.

  • T read(String id): This method reads the object which is referenced by the identifier passed as a parameter, from the global context. If the specified identifier refers to an object that is locked by other execution, this method will wait until the lock ends to return the requested information.

  • IJidokaGlobalContextHolder<T> readAndWrite(String id, Function<T, T> function): This is an atomic operation that reads the object referenced by the identifier passed in the first parameter and writes it after applying the function specified in the lambda expression passed in the second parameter. The method returns an aggregation with the original object and the modified one. If the specified identifier refers to an object that is locked by other execution, this method will wait until the lock ends to return the requested information.

  • T write(String id, T value): This method writes the object on the global context, overwriting it in case it already exists. If the specified identifier refers to an object that is locked by other execution, this method will throw an exception JidokaGlobalContextBlockedByAnotherExecutionException

  • boolean remove(String id): removes an object identified by the parameter id from the global context. It returns true only if the object is actually removed.

IExecution

The IExecution interface provides methods to get the values from executions, as well as other interesting features, such as programmatically launching robots.

It is necessary to differentiate between a current execution and previous executions, for which we must use, respectively, ICurrentExecutionDetail and IPreviousExecutionDetail interfaces.

Methods to retrieve information

There are basic methods to retrieve information from the robot, as well as its executions, either the current execution (ICurrentExecutionDetail) or the list of previous executions (IPreviousExecutionDetail). The number of executions of the last one depends on the value specified when calling getExecution.

  • String getRobotName(): Returns the id of the robot. It's a unique identifier set when the robot is created.
  • ICurrentExecutionDetail getCurrentExecution(): From the current execution you will be able to get the execution number, the date, the description, the resource and the parameters. Since it is the current execution, the data we can get differ from the previous executions.
  • List<IPreviousExecutionDetail> getPreviousExecutions(): From previous executions, we can also get the result, the total number of items processed, the total number of items with an OK result and the total number of items with a warning result. This is so because they are finished executions.
  • List<IRobotExecutionDetail> robotExecutions(); Gets all executions that are queued or running at the current time. As in the rest of the platform, the permission system is still valid so that only robot executions that share a permission with the robot that makes the call will be accessed.
ICurrentExecutionDetail

This is the interface representing the current robot's execution. To get an instance of this interface for a specific robot, you must do it as follows:

Get the current execution of a robot

1
ICurrentExecutionDetail currentExecution = server.getExecution(0).getCurrentExecution();

A value of 0 is passed in to the getExecution(int n) method, which we have already seen in the IJidokaContext interface to obtain an object of IExecution. Then, we will be able to get a reference to ICurrentExecutionDetail. The most interesting information we can obtain through this interface is provided by the following methods:

  • int getExecutionNumber(): Returns the execution number.
  • long getExecutionDate(): Returns the time when the execution started as a long.
  • String getExecutionDescription(): Returns the description of the execution.
  • Map<String, String> getParameters(): Returns the parameters passed to the execution as a Map, whose key is the name of the parameter and the value is a String that represents the real value of the parameter.
  • String getPlanetName(): Returns the resource identifier on which the robot is executed.
  • String getPlanetCustomName(): Returns the resource name in which the robot is executed.
  • String getUsername(): Returns the user name who launched the execution.
  • boolean isTesting(): Tells if the execution is marked as testing or not.
IPreviousExecutionDetail

This interface gets information about a finished execution. For this reason, it will return more information than the ICurrentExecutionDetail interface, since data about the execution results is already known.

To get information about previous executions, we need to call the getExecution(int n) method of IJidokaContext indicating the number of finished executions to retrieve. As in the previous example, the code to get this information is as follows:

1
List<IPreviousExecutionDetail> previousExecutions = server.getExecution(10).getPreviousExecutions();

Because, potentially, we're going to get information about several executions, the getPreviousExecutions() method returns a list of objects of type IPreviousExecutionDetail.

In this example, since we pass 10 as the parameter value, we'll obtain the last 10 executions of the robot, if exist. This list is sorted from the most recent to the oldest.

Apart from all the methods present in the ICurrentExecutionDetail interface, we can find the following ones:

  • EExecutionResult getResult(): Returns the result of the execution, represented by the EExecutionResult enumerator.
  • int getItemsTotal(): Returns the total number of items that were processed in the execution.
  • int getItemsOk(): Returns the number of items processed with OK.
  • int getItemsWarn(): Returns the number of items processed with warnings.
IRobotExecutionDetail

This interface shows information about executions that are currently in progress or planned waiting for the resource.

In order to obtain this information, it is necessary to invoke the robotExecutions() method of the execution obtained through the method getExecution(int n)

1
List<IRobotExecutionDetail> actualExecutions = server.getExecution(0).robotExecutions();

Shows the same information than the interface above, IPreviousExecutionDetail, with two more methods:

  • String getRobotName(); Returns the robot's id
  • Map<String, String> getResultProperties(); These properties represent the global result status from a functional point of view, it can be used to easy extraction of information from other computational systems

Methods for launching robots

To programmatically launch robots, the interface exposes an overloaded method launchRobot. Depending on the number of parameters used, it allows you to launch the robot, to launch it on the same resource as the current robot, and to launch the robot with parameters, so that we can inform the possible parameters that were registered on the console's instructions section.

  • void launchRobot(String robotName): This method launches the robot whose name matches the robotName.

  • void launchRobot(String robotName, boolean mustBeHere): This method launches the robot on the current resource or on any other resource with the necessary permissions, depending on whether mustBeHere is true or false.

  • void launchRobot(String robotName, boolean mustBeHere, String description, List<ExecutionParameter> parameters): This method launches the robot with the specified parameters. It receives a list of parameters that must be loaded into a list. The parameter's attribute name must match the name given to the instruction in the console.

  • robotName(String name): defines the robotic process name.
  • mustBeHere(boolean b): indicates that the current resource must be used or any other else.
  • node(String n): indicates the name of the resource where the robotic process must be launched.
  • description(String d): description of the execution.
  • *parameters(List p)*: parameters for the robotic process.
  • deferredDate(long l): specifies that the execution must be launched in the future as defined by the parameter.
  • executionsToLaunch(int n): specifies how many executions have to be launched.
  • queueId: assigns a pre-selected queue when the execution is launched.

  • void launchRobot(String robotName, String resource, String description, List<ExecutionParameter> parameters): This method launches the robot on the specified resource. If the parameter resource is null, the robot will run on the first available resource. You can specify execution parameters.
LaunchOptions interface

All these methods can be emulated with the next one:

  • void launchRobot(LaunchOptions options)

With the new support class, LaunchOptions, it is possible to define, apart from what robot and resource, the options we can see in the execution options window. Among them, there are the following:

  • robotName(String name): defines the robot name.
  • mustBeHere(boolean b): indicates that the current resource must be used or any other else.
  • node(String n): indicates the name of the resource where the robot must be launched.
  • description(String d): description of the execution.
  • parameters(List<ExecutionParameter> p): parameters or instructions for the robot.
  • deferredDate(long l): specifies that the execution must be launched in the future as defined by the parameter.
  • executionsToLaunch(int n): specifies how many executions have to be launched.
  • queueId: assigns a pre-selected queue when the execution is launched.

Remember that a robot execution is subject to the availability of a free resource. Therefore, the execution will queue awaiting a resource, similar to the manual launching from the console.

Rest of the methods

Additionally, we can find the following methods inside the IExecution interface:

  • boolean isRobotAllowedNow(String robotId): Checks that the specified robot can be executed attending the restrictions in the console configuration.
  • Map<EPlanetToRunRobot, List<String>> planetsForRobot(String robotId): Obtains a Map with the list of all the resources and their status related to the robot passed as parameter. When null is passed, we want to get the information for the current robot. This method is useful to know what resources are compatible with a robot.
  • IExecutionPendingWithHigherPriority existsExecutionPendingWithHigherPriority(): Checks that one or more pending executions with higher priorities exist and are compatible with the current resource.

Log

This interface provides the methods that allow you to send log information to the server. This information will be shown in the execution log and it can be of type: trace, debug, info, warn, error or fatal. Our object server can send log messages, through the following methods, in order of increasingly severity:

  • server.trace("This is a trace message");
  • server.debug("This is a message provided by the debugger");
  • server.info("Informative message");
  • server.warn("Warning message");
  • server.error("This is an error message");
  • server.fatal("A fatal error has occurred");

Messages should be sent to the server properly. For example, the debug messages help us with the development, the info messages help us with the monitoring and understanding the log.

As you can see, the interfaces IJidokaContext<T>, IJidokaStatistics and Log provide for great versatility to the interface IJidokaServer, when it comes to communicating with the server, resuming the task that a robot was doing (avoiding starting an interrupted task over again) and other actions that help monitoring the robot's operation (such as sending screen shots in moments that may be of our interest).

IJidokaRobot and Windows

These two interfaces are very important, since they represent the robot.

IJidokaRobot offers, basically, methods that allows you to use the keyboard and the mouse, as well as other features such as pausing the execution in order to adapt the working speed to a human's working pace when necessary.

IWindows, which inherits from IJidokaRobot all its functions and features, offers windows low-level handling and control support. It is suitable for robots running on Windows operating system.

A robot which is running on Windows systems could use:

1
IWindows windows = IWindows.getInstance(this);

This allows, in addition to the methods of IJidokaRobot, accessing the windows handling and control offered by IWindows, although the line::

1
IJidokaRobot windows = IJidokaRobot.getInstance(this);

will return an IWindows instance on Windows systems and an IJidokaRobot instance on other systems (Mac or Linux). Additionally, as mentioned before, the instance will be obtained from JidokaFactory. Appian RPA instances these interfaces as follows:

  • When you useIWindows.getInstance(this), IWindows will always request an IWindows instance to JidokaFactory.
  • When you use IJidokaRobot.getInstance(this), IJidokaRobot will determine which instance will be required to JidokaFactory depending on the running operating system. If it is Mac, an IJidokaRobot instance will be returned, and if it is Windows it will return an IWindows instance. If it is Linux, it will return an ILinux instance.

On Mac, we should use the second line (IJidokaRobot.getInstance(this) and on Windows or Linux we should use one or another depending on whether we need the features of either IWindows or ILinux or not.

With this clearly in mind, we will define the methods provided by IJidokaRobot, understanding that they are inherited by IWindows, to later detail the methods of IWindows itself.

Other methods of IJidokaRobot

IJidokaRobot provides other methods to help us in other common tasks.

Putting/getting information into/from the clipboard

Putting and getting information from the clipboard is something we often do. Appian RPA helps us in this task with the methods clipboardSet(String value), which saves the value into the clipboard, and clipboardGet(), which returns a String with the information contained in the clipboard.

Repeating actions

This allows you to perform an action repeatedly. This method receives three arguments: a lambda expression with the action, the number of repetitions and the pause duration between iterations.

1
windows.repeat(IWorker action, int repetitions, long millis)

Among other things, this method is helpful to perform repeated keystrokes after which you want to pause for each iteration. For example:

1
windows.repeat(() -> keyboard.tab(), 3, 1000);

This sentence will press the tab key three times, waiting 1 second between each keystroke. IJidokaRobot has even more methods, such as antiScreenSaver() to perform a slight mouse movement to prevent the screen saver to start or stops it if it is active. Along with this method there are activateScreenSaver() and deactivateAntiScreenSaver(), used to activate and deactivate this feature.

Others, like waitCondition, will be seen in detailed due to its importance.

IWindows

The interface IWindows, which inherits from IJidokaRobot, provides a lot of specific Windows functionalities. If our robot is going to run on a Windows operating system and we are going to operate applications which run on Windows, we should use this interface. The new UIAutomation module is preferred for handling Windows over this interface.

1
2
3
IWindows windows = IWindows.getInstance(this);

IWindows windows = IJidokaRobot.getInstance(this);

These two previous lines are valid to get an instance of IWindows on Windows environments.

IWindows uses JNA (Java Native Access), for easier windows handling.

IWindows offers the following methods:

  • HWND getForegroundWindow(): HWND provides a Handler for the application window in the foreground.
  • String getActiveWindowTitle(): gets the active window's title in String format.
  • HWND getActiveWindow(): gets the active window's HWND, which is only valid it we are the window's owners. Most of the times we should use getForegroundWindow().
  • boolean isWindowVisible(HWND hWnd): this method determines whether the window represented by hWnd is visible or not.
  • boolean activateWindow(String regexp): this one activates the window specified by regexp, which is a regular expression to find the window by its title. For example, when you open the Notepad, the application window title is "Untitled - Notepad". We can use this method:
1
robot.activateWindow("Untitled - No.*");
  • WindowInfo getWindow(String regexp): gets the object WindowInfo, from a regular expression. WindowInfo implements IActionable, so we can use the IActionable methods in the window. For example, the line:
1
robot.getWindow("Untitled - No.*").mouseLeftClick(new Point(100,50));

performs a click with the left mouse button on the coordinates (100, 50) relative to the window that we have just obtained with the method getWindow("Untitled – N.*"), whereas the line:

1
robot.mouseLeftClick(new Point(100,50));

performs a click with the left mouse button on the coordinates (100, 50) of the screen, which most likely won't match the previous coordinate.

  • String getWindowText(HWND hWnd): this method obtains the text from the window or control specified by hWnd.
  • RECT getWindowRect(HWND hWnd): this one obtains the object RECT (com.sun.jna.platform.win32.WinDef.RECT) from the window specified by hWnd.
  • WINDOWINFO getWindowInfo(HWND hWnd): this method gets the object WINDOWINFO (com.sun.jna.platform.win32.WinDef.WINDOWINFO) from the window specified by hWnd.
  • String getWindowClassName(HWND hWnd): this method returns the Windows class name of the window specified by hWnd.
  • WindowInfo getWindowInfoClass(HWND hWnd): this one returns an instance of WindowInfo from the HWND specified by hWnd.
  • boolean showWindow(HWND hWnd, EShowWindowState cmdShow): this is one of the methods that you will most frequently use. It will show the window specified by hWnd in the way specified by cmdShow, of type EShowWindowState. This is an enum type which contains several ways of showing a window. The possible values are:
    • SW_FORCEMINIMIZE: it makes a window to be minimized, even though the process is not responding.
    • SW_HIDE: it hides the window and activates the next one.
    • SW_MAXIMIZE: it maximizes the window, but it does not activate it.
    • SW_MINIMIZE: it minimizes the window and activates the one immediately above it in the desktop windows queue.
    • SW_RESTORE: it activates the window and shows it. If the window is minimized or maximized, it is restored it to its original position and size.
    • SW_SHOW: it activates the window and shows it on its current position and with its current size.
    • SW_SHOWDEFAULT: it shows the window on the position and size that were determined when the process that created the window was launched.
    • SW_SHOWMAXIMIZED: it activates the window and maximizes it.
    • SW_SHOWMINIMIZED: it activates the window and minimizes it.
    • SW_SHOWMINNOACTIVE: just like SW_SHOWMINIMIZED but it does not activate the window.
    • SW_SHOWNA: just like SW_SHOW but it does not activate the window.
    • SW_SHOWNORMAL: it activates and shows the window. If the window is minimized or maximized, it is restored it to its original position and size.
    • SW_SHOWNOACTIVATE: it shows the window using its most recent position and size, but it does not activate it.
  • List<WindowInfo> enumWindows(): this method obtains a list of objects WindowInfo with the main windows. The class WindowInfo, which also implements IActionable, is a container which stores the information related to a window. Among other things, it allows you to get an object Rectangle (java.awt.Rectangle) of the window, perform mouse operations on the window, and obtain and modify the object WINDOWINFO (com.sun.jna.platform.win32.WinUser.WINDOWINFO).
  • List<WindowInfo> enumChildWindows(HWND parent): similar to the previous method, in this case it gets a list of objects WindowInfo, each of which is a child of a window identified by the handle parent.
  • boolean destroyWindow(HWND hWnd): this method closes and clears the window identified by hWnd.

The following methods are related to Windows controls:

  • HWND getControl(HWND hWndParent, int id): this method gets the handler (HWND) of a Windows control whose container is the one identified by hWndParent, using the control id.
  • int getControlId(HWND hWndControl): this method gets the control id of the control identified by hWndControl.
  • boolean selectControlText(): this method selects the text within a control.
  • IField getField(Point location): this one obtains a text field located on the position specified by location. This method should only be used to work with text fields (Edit). The interface IField contains the following methods:
    • String getValue(): it gets the text field value.
    • default void setValue(String value): it sets the text field value.
    • void setValue(String value, boolean clean): it sets the text field value, after erasing the previous text.
  • ICombo getCombo(HWND hWnd): this method returns an object ICombo which enables accessing to this kind of controls. The parameter hWnd should correspond with a Windows type ComboBox control. The methods of ICombo may not work if the HWND which has been passed as a parameter is of any other Windows type or class. The interface ICombo has the following methods:
    • String getValue(): this method obtains the text from the currently selected item within the ComboBox.
    • void setValue(String value): this method establishes the selected item based on its text.
    • int getValueIndex(): this method obtains the currently selected item index (0-based).
    • void setValueIndex(int index): this method establishes the selected item based on its index (0-based).

The following methods are related to the mouse and the pointer:

  • CursorInfo getCursorInfo(): this method obtains the current object CursorInfo. CursorInfo stores information about the mouse pointer. There are two important properties concerning CursorInfo:
    • CursorInfoStruct info is a container for the pointer structure, which inherits from com.sun.jna.Structure.
    • ECursorType type is an enum type which contains the different pointer types. The Appian RPA client provides information about the current pointer type through the menu "Element Inspector".
  • boolean isCurrentCursor(ECursorType cursorType): this method returns TRUE if the current pointer type is the one specified by cursorType. Sometimes it may be of our interest to know if the mouse pointer has changed after moving it to a specific position, for example.
  • String copyAndGet(): this method represents the key combination Ctrl + C. It copies the current selection to the clipboard. If it cannot access the clipboard or the selection is empty, it will return null.
  • String cleanCopyAndGet(): similar to the previous method, but this one previously cleans the clipboard. It returns null under the same conditions as the previous method.

Next, let's have a look at the processes-related methods:

  • List<Process> enumProcesses(): this method returns a list of processes, which are represented by the class Process. This class is a representation of a Windows process.
  • void killProcess(String name): this method ends a process by its name. The Process property name returns the process's name including the .exe extension.
  • void killAllProcesses(String name, long pauseBetweenProcess): this method ends all processes specified by name, making a pause that lasts pauseBetweenProcess (expressed in milliseconds) after each process ending.
  • Process getProcess(String name): this method obtains the process whose name is name.

Other interesting methods are:

  • void setNumLock(boolean active): this method activates or deactivates NumLock depending on the active value.
  • void sendRawKey(int key, boolean pressed, int scanCode): this is a very low-level method that allows you to interact with the keyboard through the scan code associated with the key. This method is only valid for Windows systems. The parameters have the following meaning:
    • key: it corresponds with the values of the Java class java.awt.event.KeyEvent. It only takes effect when scanCode is 0.
    • pressed: it allows you to indicate whether you are pressing or releasing the key.
    • scanCode: this is the value to be used by Windows to obtain the real key to be used, provided its value is different from 0.

For example, to press the intro key in the numeric keypad we should run the following code snippet:

1
2
windows.sendRawKey(0, true, 0xe01c);
windows.sendRawKey(0, false, 0xe01c);
  • void resetModKeys(): this method will make the operating system release the keys "alt", "shift" and "control" in case any of them is pressed.
  • boolean virtualKeyPressed(int virtualKey): this method returns the key status. For example, you can check if the keys "alt", "shift" or "control" are pressed. The parameter may correspond with the values of the Java class java.awt.event.KeyEvent, though this Java class does not cover all the possibilities and you can also indicate some other values.
  • void setCapsLock(boolean active): Depending on the value of active, this method activates or deactivates CapsLock.

Keyboard

To emulate the keyboard use, Appian RPA can work with the so-called scan codes, which can be seen as the "physical codes" associated with the keys.

Due to the diversity of keyboard distributions for different languages on the operating system, the scan codes may not represent the key that you want to press.

A good example would be the character "?". Since the key assigned to Spanish is different from the one assigned to English, if the operating system is working in English, the received character would be "_", which is in the same keyboard position.

In such situations, you can change the keyboard distribution to be used, so that the emulation is done through the key combination ALT+<numeric pad>. Since the platform version 3.9.0 such option is available by using the method setVariant of IJidokaRobot

Whenever a character is not supported an exception will be thrown, regardless of the keyboard distribution you are using. These issues should be reported to the Appian RPA team to include that character.

We have already mentioned some of the methods of IJidokaRobot, to make it easier to use the keyboard. Now we are going into more detail.

IJidokaRobot provides the following methods for keyboard usage:

  • void typeText(String text): this method allows you to enter the text specified within text just as a human would do.
  • IKeyboardSequence getKeyboardSequence(): this method gets an instance of the interface IKeyboardSequence.
  • IKeyboardSequence keyboardSequence(): similar to the previous one, this method returns an instance of IKeyboardSequence.
  • IKeyboard getKeyboard(): this method gets an instance of the interface IKeyboard.
  • IKeyboard keyboard(): like the previous one, this method returns an instance of IKeyboard.
  • void typeText(IKeyboardSequence sequence): this method allows you to process key combinations through IKeyboardSequence.
  • void typingPause(long millis): this method sets the pause in milliseconds that the robot must perform once it has entered the text or a IKeyboardSequence. To override a default pause, you should set the parameter millis to 0.
  • long getTypingPause(): this method gets the duration in milliseconds of the pause that the robot must perform once it has entered the text or a IKeyboardSequence.
  • void setVariant(EKeyboardVariant variant): this method changes the keyboard distribution, choosing either SCAN, which is the default option, or ALT.

As you can see, the method typeText is overloaded. It admits both text (String text) and IKeyboardSequence (IKeyboardSequence sequence).

Although we already know them broadly, we are analyzing the interfaces IKeyboardSequence and IKeyboard because they will make keyboard handling much easier through IJidokaRobot.

IKeyboardSequence

IKeyboardSequence represents a keystroke combination or sequence.

Every time we press Ctrl + C to copy, Ctrl + V to paste or Alt + F4 to close a window in most applications, we are executing keystroke sequences.

This interface enables easy typing of such combinations by the robot, as well as other tasks such as entering text or caps locking.

We can chain calls to the methods of this interface (which is a technique known as Fluent interface), so that several methods make up a full key combination or text typing.

To get an instance of IKeyboardSequence:

1
robot.getKeyboardSequence();

Once we have obtained the instance, we can add the necessary methods to build our key combination.

We will use the overloaded method type to enter text and keystrokes. Let's see some examples.

Text typing and keystrokes

In this example, we want to enter some text or one keystroke by using IKeyboardSequence.

We will use the method type(String text) of IKeyboardSequence.

Although we can use typeText(String text) from IJidokaRobot for this, we include this example for clarity's sake.

The code that will apply the sequence just as a human would do is the following:

1
robot.getKeyboardSequence().type("Example text).apply();

Actually, using IKeyboardSequence to enter text is recommended if it is part of a keystroke sequence or other key combination.

As we have mentioned earlier, if we only want to enter text, it is more appropriate to use the method typeText of IJidokaRobot:

1
robot.typeText("Example text);

The method type is an emulation of a keystroke, for example:

1
robot.getKeyboardSequence().type("r").apply();

will press the "r" key.

One of the overloaded versions of the method type allows you to specify how many times you want to press the key: type(String text, int repetition):

1
robot.getKeyboardSequence().type("r", 3).apply();

This line will make the "r" key to be pressed three times.

Key sequences and common combinations

IKeyboardSequence also provides a wide range of methods that simplify certain keystrokes.

Unlike the method type which represents the keystroke (pressing and releasing), the methods press(int key) and release (int key) respectively press and release the specified key (key code). It should be kept in mind that by using the method press, the key will remain pressed until it is released with the method release. The integer representing the key in these methods must match the values provided by the Java class java.awt.event.KeyEvent.

Some frequently used combinations, such as pressing Alt + key or Ctrl + key, are made easier with specific methods, for example pressAlt() and releaseAlt(), which respectively press and release the Alt key.

This way, for example, we can emulate the sequence Alt + E with the following code:

1
robot.getKeyboardSequence().pressAlt().type("e").releaseAlt().apply();

Remember that the other alternative is:

1
robot.typeText(robot.getKeyboardSequence().pressAlt().type("e").releaseAlt());

There are other specific methods for certain keys that work like this, such as: pressShift and releaseShift, or pressControl and releaseControl.

Once again, remember that it is important to release the key that have been previously pressed.

There are also some methods to press (press and release) certain special keys, such as typeReturn, typeTab, typeControl and typeEscape. There is even a method that emulates the sequence Alt + Fx: the method typeAltF(int numberF):

1
robot.getKeyboardSequence().typeAltF(2).apply();

The previous line presses Alt+F2.

Additionally, these methods are overloaded to allow you to specify how many keystrokes you want. This way we can easily move throughout a form, for example, by using the method typeTab( int repetitions). Other methods resolve some common tasks, for example

1
robot.getKeyboardSequence().selectAll().apply();

This line is equivalent to "select all".

Other common keys

There are also methods to emulate these other keystrokes: Home, End, Page Up, Page Down and the up, down, left and right arrows.

1
2
robot.getKeyboardSequence().up().right(2).apply();
robot.getKeyboardSequence().pageDown().apply();

The previous line emulates the keystroke of the UP key and two keystrokes of the RIGHT key. The second line presses the Page Down key.

Many of these methods are overloaded and accept the integer parameter repetition to indicate the number of keystrokes needed for the specified key.

Pauses

To end with, sometimes we may need to pause within a sequence. To make IKeyboardSequence do so we have the method pause available:

1
robot.getKeyboardSequence().typeSpace().pause().type("e").typeReturn().apply();

The previous code emulates a keystroke on the space bar, followed by a pause, a keystroke on the "e" key, and the Return key.

As you can see, IKeyboardSequence is very comprehensive and makes it far easier to use the keyboard.

Working with Scan Codes

The interface IKeyboardSequence offers low level functionalities to work with the key scan codes. This allows you to interact with the less common keys or those which are not represented by a single character. For example, the keys located on the numeric pad cannot be represented like their counterparts on the conventional keyboard.

  • IKeyboardSequence pressScanCode(int scanCode): this method presses the key associated with the scanCode.
  • IKeyboardSequence releaseScanCode(int scanCode): this method releases the key associated with the scanCode.
  • IKeyboardSequence typeScanCode(int scanCode): this method presses and releases the key associated with the scanCode.

These methods work properly only on Windows systems.

For example, to press the intro key on the numeric pad we would run the following code snippet:

1
windows.getKeyboardSequence().typeScanCode(0x0e01c).apply();

IKeyboard

IKeyboard looks like IKeyboardSequence but it simplifies even more the emulation of the most common keystrokes. The main difference is that each method emulates the keystroke directly.

To get an instance of IKeyboard we can use:

1
robot.keyboard();

Some methods are like their counterparts in IKeyboardSequence, for example type (String text) or type (String text, int repetition), and some others simplify the simultaneous keystrokes, such as alt (String letter), altF (int numberF) or control (String letter).

In addition, the method pause of IKeyboard is overloaded and allows you to set the pause duration in milliseconds: pause (long millis).

IKeyboard aims to simplify some keyboard functions already provided by IKeyboardSequence.

For example, with IKeyboardSequence you could emulate the combination Ctrl + E this way:

1
robot.getKeyboardSequence().pressControl().type("e").releaseControl();

And with IKeyboard, we can simplify it to:

1
robot.keyboard().control("e");
Working with Scan Codes

Like the interface IKeyboardSequence, the interface IKeyboard allows you to work with the keys at low level with the following methods:

  • IKeyboard pressScanCode(int scanCode): this method presses the key associated with the scanCode.
  • IKeyboard releaseScanCode(int scanCode): this method releases the key associated with the scanCode.
  • IKeyboard scanCode(int scanCode): this method presses and releases the key associated with the scanCode.

These methods work properly only on Windows systems.

For example, to press the intro key on the numeric keypad we would run the following code snippet:

1
windows.getKeyboard().scanCode(0xe01c);

Mouse

IJidokaRobot inherits from IActionable, which is an interface that provides methods for mouse handling, as well as other features and access to objects that we will need to manage the mouse.

For example, the method getRectangle returns the Rectangle (java.awt.Rectangle) of the screen. An object Rectangle represents a rectangular area which is determined by x/y coordinates, representing its upper left-hand corner, its width and height, as well as many other properties and useful methods.

Moving the mouse pointer

The methods mouseMove() and mouseMove(Point coordinates), respectively, move the pointer to the object's upper left-hand corner or to the position specified by coordinates. The coordinates represented by coordinates are relative to the object's upper left-hand corner.

The method mouseMoveToCenter() moves the pointer to the object's center.

Clicking on an object

There are several methods for clicking, depending on our needs.

For example, clickOnCenter() emulates clicking on an object's center, though if we want to click on a specific point, we can use the method mouseLeftClick(Point coordinates).

Actually, the default implementation of methods click(), mouseLeftClick() and mouseLeftClick(Point coordinates) make the result of the method click() to be equivalent to mouseLeftClick() and mouseLeftClick(Point coordinates), that is, ultimately you would be running *mouseLeftClick(new Point(0,0)). *

These methods can be overwritten in our implementations depending on our needs, for example, to make the method click() to emulate clicking on the current pointer position or on the upper left-hand corner.

Similarly, to click() and mouseLeftClick(…) we have the methods mouseRightClick() and mouseRightClick(Point coordinates) to emulate right clicking.

Double clicking on an object

We also have at our disposal the necessary methods to emulate double clicking. They are the methods mouseDoubleLeftClick() and mouseDoubleLeftClick(Point coordinates).

To finish analyzing the methods of IJidokaRobot that makes it easy to use the mouse, let's mention the method mousePause(long millis), which sets a pause in milliseconds to be done by the robot when it has performed an action with the mouse. This pause is determined by the parameter millis. We have already mentioned the importance of the robot's working pace which needs to be adapted to the application it is using, which almost certainly was designed for human usage.

Pauses

Again, another method to adapt the robot's working speed to the applications it is using.

The method pause accomplishes this. It performs a default pause of the robot's execution. We can also specify the pause duration with pause(long millis).

Lastly, the method defaultPause(long millis) sets the pause's default duration in milliseconds that the robot will do each time it calls the method pause().

Apart from the previous methods we have the method characterPause(long millis). This method allows you to set a pause between characters while typing a text string. This is what we call slow typing, which best emulates the human typing.

The method waitCondition and the interface IWaitFor

We have just highlighted the importance of this method of IJidokaRobot. We have already mentioned several times that an Appian RPA Robot must adapt its working pace to the applications it's using. This means that it should consider that applications have been designed, most of the times, for being used by a human. So, they can be overtaken by a high working speed.

A common example is the robot that launches an application and tries to interact with it. It's quite probable that the application launching has not finished yet, thus being still unavailable. Therefore, our robot should wait.

This waiting time will depend on a number of factors such as the performance of the machine on which the robot is running (processor, RAM, virtual environment), the execution speed of the application itself (it must previously load certain modules or files), or the machine overload at that time.

So far, we have used the method pause (pause() and pause(long millis)), to perform a fixed pause in the robot execution, but what is appropriate is that the robot waits the time needed, not more and not less time.

Setting a fixed pause may cause that, whether the robot waits longer than necessary, or the robot does not wait long enough, hence we need a way to optimize this waiting time.

The method waitCondition and the interface IWaitFor address this issue.

The method waitCondition

The method waitCondition makes the robot wait for certain condition.

The method's signatures are:

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
<T> boolean waitCondition(int numberOfAttempts, 
    long pauseInMillisBetweenAttempts, 
    String logName,
    T context,
    ICondition<T> condition)

<T> boolean waitCondition(int numberOfAttempts, 
    long pauseInMillisBetweenAttempts, 
    String logName,
    T context, 
    boolean throwExceptionIfNotSatisfied,
    ICondition<T> condition)
    throws JidokaUnsatisfiedConditionException

<T> boolean waitCondition(Date untilDate, 
    long pauseInMillisBetweenAttempts, 
    String logName,
    T context, 
    ICondition<T> condition);

<T> boolean waitCondition(Date untilDate, 
    long pauseInMillisBetweenAttempts, 
    String logName,
    T context, 
    boolean throwExceptionIfNotSatisfied, 
    ICondition<T> condition)
    throws JidokaUnsatisfiedConditionException;

The two first methods are similar, except that the second one throws an exception of type JidokaUnsatisfiedConditionException if the condition has not been met after a specified number of attempts. Likewise, the third and fourth methods are similar too, except for the exception.

The main difference between both versions is that the two first methods base the wait on a specified number of attempts (int numberOfAttempts), whereas the two last methods use a specific moment in time (Date untilDate):

  • int numberOfAttempts: this parameter indicates how many times the robot will check whether the condition is met.
  • Date untilDate: this parameter represents the moment in time until which the checks will be done.

The common parameters are:

  • long pauseInMillisBetweenAttempts: this is the time lag between attempts, expressed in milliseconds.
  • String logName: this is the text that identifies the condition in the log.
  • T context: context.
  • ICondition<T> condition: the condition to be met.

Additionally, the parameter:

  • boolean throwExceptionIfNotSatisfied: will determine whether the exception should be thrown or not in case the condition is not met, due to either all attempts have been made, or the timeframe specified for the wait length has been reached.

Let's see an example of how to use the method.

Suppose a robot wants to open WordPad to work with it:

1
2
3
4
5
6
7
robot.waitCondition(10,
    3000, 
    "WordPad opened", 
    null, 
    true, 
    (i, context) -> 
        robot.getWindow("Document - WordPad.*") != null);

This code will make the robot wait until WordPad is open. In this case, it will make 10 checks (or attempts), waiting for 3 seconds (3000 milliseconds) after each attempt.

The condition is identified in the log by the text "WordPad opened". An exception of type JidokaUnsatisfiedConditionException should be thrown if the condition is not met after all attempts have been made. Additionally, if the condition is not met after all the attempts, a screen shot will be sent to the execution log, to check why the condition has not been met.

The condition is represented by the Lambda Expression.

1
(i,context) -> robot.getWindow("Document - WordPad.*") != null

It refers to the method:

1
boolean satisfied(int numberOfAttempt, T context);

of the functional interface ICondition<T>.

You can see, as mentioned at the beginning of this guide, that Appian RPA uses Java 8. In this case, functional interfaces and lambda expressions.

A functional interface is any interface with only one abstract method and may be annotated with @FunctionalInterface. The interface ICondition<T> of Appian RPA is a functional interface, which contains the method:

1
boolean satisfied(int numberOfAttempt, T context)

Lambda expressions allow you to simplify the code and perform in-line implementations of functional interfaces. In our example, we are passing a code snippet as a parameter. We can say that a lambda expression encapsulates a specific functionality.

Since the interface* ICondition<T>* has only one abstract method, in our case we are passing a lambda expression as a parameter to the method waitCondition:

1
(i,context) -> robot.getWindow("Document - WordPad.*") != null

The condition is that robot.getWindow("Document - WordPad.")* is not null, which means that WordPad has started properly and is available to interact with it.

The getWindow(String regexp) method belongs to the IWindows interface. It returns a WindowInfo object corresponding with the window whose title matches the regular expression in the regexp parameter. This example uses the pattern Document - WordPad.*.

In this case the condition is that robot.getWindow("Document - WordPad.*") is not null, which means that WordPad has started properly and is available to interact with it.

The method getWindow(String regexp) belongs to the interface IWindows, and we will see it later. Basically, it returns an object WindowInfo corresponding with the window whose title matches the regular expression regexp. In this case we have used the pattern "Document - WordPad.*".

The interface IWaitFor

This interface aims to simplify and make it easier to manage the robot's waits.

We can access it through IJidokaRobot, by using the methods:

  • IWaitFor getWaitFor(IRobot robot)
  • IWaitFor waitFor(IRobot robot)

Both methods are similar and return an instance of the interface IWaitFor.

Let's analyze this interface:

  • static final int DEFAULT_TIMEOUT_SECONDS = 30: this is the default timeout, expressed in seconds.
  • void defaultTimeOutSeconds(int timeOutSeconds): this method allows you to set the default timeout, through the parameter timeOutSeconds.
  • int getDefaultTimeOutSeconds(): this method gets the value of the default timeout.
Methods to wait for an image on screen
  • boolean image(File file) throws IOException: this method waits for an image, specified by file, to appear on screen for the default timeout. This means that once the timeout expires, it will stop waiting and will return false.
  • boolean image(File file, int timeOutSeconds) throws IOException: like the previous method, but this one allows you to set the duration of the timeout through the parameter timeOutSeconds.
  • boolean image(File file, float tolerance) throws IOException: this method performs the same functionality as image (File file), and also, it allows you to set the tolerance. This is a concept discussed in the Falcon module, but we can define the tolerance as "how close an image should be with respect to a pattern to accept it", that is, given an image A, which acts as a pattern and is specified in file, the robot will search on screen for an image B that matches A, accepting a minimum coincidence which is defined by tolerance. This value is of type float and will range between 0f and 1f. Typically, tolerance values of ,05f give good results. We will go into more details later.
  • boolean image(File file, float tolerance, int timeOutSeconds) throws IOException: like the previous method, but this one allows you to set the duration of the timeout through the parameter timeOutSeconds.

Other versions of the previous methods differ only on the type of the object used as a pattern for the image to be searched. This way, we can use:

  • An object InputStream:
1
2
3
4
5
6
7
8
9
10
11
boolean image(InputStream inputStream) throws IOException;

boolean image(InputStream inputStream, int timeOutSeconds)
    throws IOException;

boolean image(InputStream inputStream, float tolerance)
    throws IOException;

boolean image(InputStream inputStream, float tolerance,

int timeOutSeconds) throws IOException;
  • An object URL:
1
2
3
4
5
6
7
8
9
boolean image(URL url) throws IOException;

boolean image(URL url, int timeOutSeconds) throws IOException;

boolean image(URL url, float tolerance) throws IOException;

boolean image(URL url, float tolerance,

int timeOutSeconds) throws IOException;
  • An object ImageInputStream (javax.imageio.stream.ImageInputStream):
1
2
3
4
5
6
7
8
9
10
11
boolean image(ImageInputStream imageInputStream) throws IOException;

boolean image(ImageInputStream imageInputStream, int timeOutSeconds)
    throws IOException;

boolean image(ImageInputStream imageInputStream, float tolerance)
    throws IOException;

boolean image(ImageInputStream imageInputStream, float tolerance,

int timeOutSeconds) throws IOException;
  • There are also several methods to wait for an image to appear on screen, among a set of candidate images. When one of them is detected, the method will return that image:
1
2
IImageResource image(IImageResource... images) throws IOException
IImageResource image(int timeOutSeconds, IImageResource... images) throws IOException

Once an image has been found, the description will be sent to the execution log to inform what image has been found.

  • We have also available methods to wait for a window to appear:
    • String window(String... windowTitleRegExp): given a series of regular expressions specified by windowTitleRegExp, this method waits for a window whose title matches one of the expressions to appear, and returns the regular expression that has been met, or null otherwise.
    • String window(int timeOutSeconds, String... windowTitleRegExp): similar to the previous method, this one allows you to specify the maximum waiting time.
  • The following methods allows you to wait for windows activation:
    • String windowActive(String... windowTitleRegExp): this method waits for an active window whose title matches one of the titles specified in the parameter windowTitleRegExp, using the default waiting time. If none is found, it will return null.
    • String windowActive(boolean throwExceptionIfNotSatisfied, String... windowTitleRegExp): this method is similar to the previous one, but you can also specify whether an exception should be thrown if nothing is found, or not.
    • String windowActive(int timeOutSeconds, String... windowTitleRegExp): this version of windowActive allows you to specify the waiting timeout as a parameter.
    • String windowActive(int timeOutSeconds, boolean throwExceptionIfNotSatisfied, String… windowTitleRegExp): this version of windowActive joins the three previous functionalities: you can specify the waiting timeout and whether an exception should be thrown or not if no active window is found.
    • String windowActive(int timeOutSeconds, boolean throwExceptionIfNotSatisfied, boolean saveScreenIfNotSatisfied, String... windowTitleRegExp): this method is similar to the previous one, and you can also specify whether a screen shot should be saved if no active window is found, which will be sent to the robot's execution log.
  • You can also wait for mouse pointer changes in appearance. The Appian RPA enum type ECursorType represents the possible kinds of mouse cursor available. For further information please refer to LoadCursor function.
    • ECursorType cursor(ECursorType... types): this method waits until the mouse pointer icon turns into one of the values of ECursorType specified by types, and returns the ECursorType value detected, or null if no match was found after the default waiting timeout.
    • ECursorType cursor(int timeOutSeconds, ECursorType... types): like the previous method, but this one allows you to set the duration of the timeout through the parameter timeOutSeconds.
  • With these methods you can wait for files to exist:
    • File file(File... files) throws IOException: this method waits until one of the specified files exists, and returns the matching file, or null if no file has been found after the default timeout.
    • File file(int timeOutSeconds, File... files) throws IOException: like the previous method, but this one allows you to set the duration of the timeout through the parameter timeOutSeconds.

The method wait and the interface IEasyCondition deserve a detailed explanation:

The method wait

IWaitFor provides the overloaded method wait:

  • boolean wait(String logName, IEasyCondition condition).
  • boolean wait(int timeOutSeconds, String logName, IEasyCondition condition).

This method enables the evaluation of an IEasyCondition. We can specify the descriptive text for the execution log through the parameter logName. In the second version, we can also set the waiting timeout (timeOutSeconds) expressed in seconds, which is the maximum time our robot will be waiting for the condition to be met.

IEasyCondition is a functional interface for building conditions that should be met, implementing the method boolean satisfied() in the same way that we saw earlier with the interface ICondition<T>.

For example, suppose we want our robot to wait 20 seconds for a window named "Title example" to be active. To get the active window's title we will use getActiveWindowTitle() of the interface IWindows, which we will see later, and returns the active window's title.

1
2
3
4
IWaitFor wf = robot.getWaitFor(this);
    wf.wait(20,
    "Waiting until the window "New document" is in the foreground ", 
    ()-> robot.getActiveWindowTitle().matches("New document"));
  • timeOutSeconds: 20
  • logName: "Waiting until the window \\"Title example\\" is in the foreground"
  • condition: () -> robot.getActiveWindowTitle().matches("New document")

In this example, we have set:

  • timeOutSeconds: 20
  • logName: "Waiting until the window \"Title example\" is in the foreground"
  • condition: () -> robot.getActiveWindowTitle().matches(".Example Title.")

The condition to be met, for which the robot will wait 20 seconds at most, is that the active window's title is or contains "Example Title", and we have built it using a lambda expression:

1
()-> robot.getActiveWindowTitle().matches("New document");

We have already mentioned that lambda expressions are new in Java 8.
In any case, IWaitFor provides some methods that can make it easier for us:

  • IEasyCondition windowExists(String windowTitleRegExp): this condition will be met if there is a window whose title matches the regular expression windowTitleRegExp.
  • IEasyCondition cursorIs(ECursorType type): this condition will be met when the type of the mouse pointer is the one specified by type.
  • IEasyCondition fileExists(File file): this condition will be met when the file specified exists.

Lastly, we can logically combine conditions IEasyCondition. This means that we can create conditions whose fulfillment depends on other conditions, the same way that we use the logical operators AND, OR and NOT.

To do so, the interface IWaitFor includes three methods whose parameters are one or more conditions IEasyCondition:

  • IEasyCondition and(IEasyCondition... conditions): this method returns one IEasyCondition that will be met if all the IEasyCondition passed as parameters are met (are true).
  • IEasyCondition or(IEasyCondition... conditions): this one returns one IEasyCondition that will be met if any of the IEasyCondition passed as parameters is met (is true).
  • IEasyCondition not(IEasyCondition condition): this method returns one IEasyCondition that will be met if the IEasyCondition received as a parameter is not met (is false).

We will see an example which includes these three methods: an IEasyCondition whose fulfillment will depend on other IEasyCondition.

In our example of the IEasyCondition usage, we will use these three conditions:

  • A: the file "example.txt" must exist.
  • B: the window with the title "example.txt – Notepad" must be visible.
  • C: we have already processed the 100th item (suppose there is a variable int currentItem that stores the current item).

Let's create the corresponding IEasyCondition.

The condition A will be met if the file "example.txt" exists, so we can use the method fileExists(File file) provided by IWaitFor:

1
wf.fileExists(new File("example.txt"));

The condition B will be met if a window with the title "example.txt – Notepad" is visible. In this case, we must create our IEasyCondition. We will use the method isWindowVisible(HWND hWnd) of IWindows. Since we have not seen this interface yet, we anticipate that this method accepts as a parameter a Windows handler to identify the window whose visibility we need to know. To get this handler, we use the method getWindow(String regexp). These methods and the interface IWindows will be seen in detail in IWindows

We will build the condition as a lambda expression, that will only work in the proper context:

1
2
() -> robot.isWindowVisible(
    robot.getWindow("example.txt.*").gethWnd())

The condition C is quite simpler. We have assumed that there is an integer type variable that stores the current item: int currentItem, and we have established that, to meet the condition, we must have already processed the 100th item. Our IEasyCondition, as a lambda expression, will be like this:

1
() -> currentItem > 100

Now let's code several "nested" IEasyCondition:

  • At least one of the three conditions, A, B, C must be met, that is, it's enough for us if any of them is met
1
2
3
4
5
6
7
wf.wait(
    20, "At least one of A, B, or C", 
    wf.or(
    wf.fileExists(new File("example.txt")), 
    () -> robot.isWindowVisible(robot.getWindow("example.txt.*").gethWnd()),
    () -> currentItem > 100)
);

For clarity, we have put each IEasyCondition in a different line. We have passed the three conditions as parameters to the method or (IEasyCondition… conditions).

  • The three conditions A, B and C must be met. In this case, we just need to use the method and (IEasyCondition… conditions):
1
2
3
4
5
6
7
8
wf.wait(20, 
    "At least one of A, B, or C", 
    wf.and(
        wf.fileExists(new File("example.txt")), 
    () -> robot.isWindowVisible(
        robot.getWindow("example.txt.*").gethWnd()),
    () -> currentItem > 100)
);

We can create as many combinations as needed, of greater or lesser complexity.

If we need more complex conditions, we can create our own classes which implement the interface IEasyCondition:

1
2
3
4
5
6
7
8
private class MyCondition implements IEasyCondition {

    @Override
    public boolean satisfied() {
        // Implement condition here
        return false;
    } 
}

to use them later:

1
2
3
4
5
IWaitFor wf = robot.getWaitFor(this);

MyCondition cond = new MyCondition();

wf.wait(20, "My condition", cond);

IMail

This interface allows you to send and receive mails. You can send mails from a resource or from the Appian RPA platform server. You can also attach files to the mails or send the content in HTML format or plain text.

It is worth highlighting that sending mails from the server or the resource only has an impact on which machine should have access to the mail server we are using from the robot. In other words, the only difference is the machine from which mails are sent.

To get an instance of the interface we must use the following method:

1
IMail mail = IMail.getInstance(this);

This interface has the following methods:

  • void sendMail(MailSendOptions options): this method sends an email with the information provided by the parameter options. The mail is sent from the resource on which the robot is running.

  • void sendMailFromServer(MailSendOptions options): this method differs from the previous one in that the mail is sent from the Appian RPA platform server.

  • ReceivedEmail[] receiveMail(MailReceiveOptions options): this method checks the mailbox specified by the parameter options and retrieves all existing messages.

As you can see, these methods use the classes MailSendOptions and MailReceiveOptions. Both classes have the necessary information to connect with the mail server. These classes provide a fluid API that allows you to easily define all the necessary information.

The class MailSendOptions allows you to send mails in HTML format or plain text, depending on how the mail content is set. It also allows you to specify whether you want to use a cached session to send the mails from the server. Let's see some of its methods:

  • MailSendOptions sslHost(boolean sslHost): this method allows you to specify whether the server connection will use SSL.
  • MailSendOptions host(String host): this one specifies the server that will be used to send the mails.
  • MailSendOptions port(int port): this one specifies the port to which the mail server is listening.
  • MailSendOptions username(String username): this method allows you to specify the username to access the mail server.
  • MailSendOptions password(String password): this method allows you to specify the password to access the mail server.
  • MailSendOptions fromAddress(String fromAddress): this one specifies the mail address from which the mails will be sent.
  • MailSendOptions toAddress(String... toAddress): this one allows you to specify the list of recipients of the mail.
  • MailSendOptions subject(String subject): this one specifies the mail subject.
  • MailSendOptions textContent(String textContent): this one allows you to specify the mail body in plain text.
  • MailSendOptions htmlContent(String htmlContent): this one allows you to specify the mail body in HTML format.
  • MailSendOptions attachments(MailAttachment attachment): allows adding attachment files to the mail by using the class MailAttachment.

An example to send a mail with HTML content would be as follows:

1
2
3
4
5
6
7
8
9
10
11
12
// Create mail options from parameters
mailSendOptions = new MailSendOptions();

mailSendOptions.toAddress(server.getParameters().get("notificationTo"))
        .fromAddress(server.getParameters().get("notificationFrom"))
        .subject(server.getParameters().get("notificationSubject"))
        .host(server.getParameters().get("mailServerHost"))
        .port(Integer.parseInt(server.getParameters().get("mailServerPort")))
        .username(server.getParameters().get("mailServerUser"))
        .password(server.getParameters().get("mailServerPassword"));
        
mail.sendMailFrom(mailOptions);

Using the Client module

In the previous sections, we have discussed this module's classes, interfaces and methods to a greater or lesser extent. Now it's time to show some basic problems and tasks that a robotic process could be required to perform, and how to solve them with the Client module. We will show some code snippets as a demonstration.

These examples assume that the robot is going to run on a machine with Windows 7 or 10 operating system. Additionally, we assume that the Appian RPA client is in a folder called Appian RPA, within C:

1
C:\Appian RPA

The following two objects will be repeatedly used in the code snippets of our examples:

  • IWindows robot, which will be our robotic process.
  • IJidokaServer<?> which is the interface to communicate with the server, and other tasks.

As mentioned earlier, IWindows is valid for Windows environments and we can initiate it in two ways

1
2
IWindows windows = IJidokaRobot.getInstance(this);
IWindows windows = IWindows.getInstance(this);

Where this refers to a class in our project that implements IRobot, or, in other words, it represents a Appian RPA Robot.

Additionally, we establish the duration of the pauses to be made when using the keyboard and the mouse:

1
2
windows.typingPause(PAUSE);
windows.mousePause(PAUSE);

We already know the initialization of IJidokaServer. In this case, we won't use a context class, at least for now:

server = (IJidokaServer<?>)JidokaFactory.getServer();

Remember that in the following code snippets, the method IJidokaRobot.typeText(String text) is used, but you may want to use IKeyboard.type(String text).

Creating and saving a text file with Notepad

In this example, we are creating and saving a text file called example.txt in our robot's workspace.

The first thing that comes to mind when we think of creating the file, is to use the Windows run dialog, which can be opened by pressing Windows + R. After that, we can write "notepad" and press return.

Build the file path

1
2
3
4
// Build the whole path to the file
String file = "notepad_example.txt"; 
String ruta = Paths.get(server.getCurrentDir(), file).
        toString();

We have created the path to the file example.txt by using the class Paths.

We get the robot's working directory through server.getCurrentDir(). Now we have the information stored in the variable path.

Opening the Windows run dialog with Windows + R

1
2
3
4
5
// Insert text in the log
server.info("Creating notepad_example.txt");
// Windows + R
windows.typeText(windows.getKeyboardSequence().pressWindows().
        type("r").releaseWindows().pause());

First, we have sent to the server the information about what we are going to do, though as you know this is optional.

Then, we have built an IKeyboardSequence, whose keystroke sequence is: press Windows + press R + release Windows.

IKeyboardSequence will automatically make a pause at the end of the sequence

Opening the Notepad

1
2
3
4
5
6
// Type "notepad"
windows.typeText("notepad");
// Type Return
windows.typeText(windows.getKeyboardSequence().typeReturn());
// Explicit pause to wait for Notepad
windows.pause(PAUSE);

Once we have opened the run dialog, we type the text "notepad" and press Return. The Notepad will open. We have added a pause to allow enough time for the application to open.

Saving the file as example.txt

By pressing Ctrl + S we will show the dialog box "Save as…".

1
2
3
4
/* Save the file by typing Ctrl+S to open the dialog*/
windows.typeText(windows.getKeyboardSequence().pressControl().
        type("s").releaseControl());
windows.pause(PAUSE);

Once we have opened the dialog box, we must type the name with which we want to save the file, as well as the folder in which we want to save it.

The full path must be typed in the field "Name". We can set the focus on this control by pressing Alt + N.

1
2
3
4
5
6
/*
 * Now we have have to find the control "Name". By pressing Alt+N
 * we obtain the focus
 */
windows.typeText(windows.getKeyboardSequence().pressAlt().
                type("n").releaseAlt());

We want to save the file in the robot's working folder, so we will use the class Paths as a combination of the robot's working directory, server.getCurrentDir(), and the desired name, String file = "example.txt".

We type the full path in the field "Name" and press Return.

1
2
3
4
5
6
7
8
9
// Build the whole path to the file
String file = "notepad_example.txt"; 
String path = Paths.get(server.getCurrentDir(), file).
        toFile().getAbsolutePath();
// Write the path to the file
windows.typeText(path);
// Press Return
windows.typeText(windows.getKeyboardSequence().typeReturn()); 
windows.pause(PAUSE);

We make a pause at the end to allow enough time for the system to save the file.

It should be considered that this could not be the first time our robot runs.

Therefore, the file could already exist, unless we have deleted it. So, we should know if Windows is asking us to replace the existing file. In such case, we will answer yes:

1
2
3
4
5
6
7
8
9
10
/*
 * If the file already exists, it is replaced.
 * To know when the question is shown, we must check that the active
 * window is not Notepad.
 */
if (!windows.getActiveWindowTitle().contains("notepad_example")) {
    // Press Yes
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("y").releaseAlt());
}

We can check this in several ways. In this case, we have chosen to determine whether the active window's title contains the text "example.txt". The explanation is simple: when Notepad opens to create a new document, its title is "Untitled – Notepad". If the file has been properly saved, the active window's title contains "example.txt – Notepad", hence if the active window contains "example.txt" it will mean that the file has been saved, and otherwise, Windows will be waiting our confirmation to replace the file.

Creating and saving a file with WordPad

This time, we are creating a new text file using WordPad. We have chosen WordPad because it is included in Windows 7 and 10 and it allows us to do more things than the Notepad in our examples.

Here we are not going to enter any text. We will simply create the file and save it in the robot's folder as "example_wordpad.rtf".

First, we should find the easiest way to perform the task, because this will help our robot to be more reliable, that is, it will be able to properly do what it must do.

A good choice could be to open WordPad and save the file with the chosen name in the robot's working directory, through "Save as…".

Opening WordPad through Run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Insert text in the log
server.info("Creating the example file wordpad_example.rtf");
// Open the dialog box to run win Win + R
windows.typeText(windows.getKeyboardSequence().pressWindows().
        type("r").releaseWindows());
windows.pause(PAUSE);
// Type wordpad
windows.typeText("wordpad");
// Press Return to open Wordpad
windows.typeText(windows.getKeyboardSequence().typeReturn());
// Wait until WordPad is opened
windows.waitCondition(10, 3000, "WordPad opened", null, true, 
        (i,context) -> 
            windows.getWindow("Document - WordPad.*") != null);

We start by sending information to the execution log and entering Windows + R through an IKeyboardSequence to show the Windows Run dialog. Then, we type "wordpad" and press Return to launch WordPad. Then, we wait for WordPad to open by using waitCondition(…), so that we wait the necessary time.

In the example above, we have set 10 tries, every 3 seconds. This is what will be shown in the execution log:

1
2
[DEBUG] 20:19:10 18s (0h) - Waiting for condition "WordPad opened", try #1 of #10
[DEBUG] 20:19:11 18s (0h) - Condition "WordPad opened" satisfied with #1 of #10 attempts

In this case, the condition has been met in the first try. If it hadn't been that way, it would have kept trying every 3 seconds until reaching 10 checks.

There is a simplified version of waitCondition(…) accessible from the interface IWaitFor.

Opening the dialog "Save As…"

At this point, we can see an empty WordPad document on screen. Let's show the dialog "Save As…" by pressing Ctrl + S.

1
2
3
4
// Save the file by pressing Ctrl+S to open the dialog
windows.typeText(windows.getKeyboardSequence().pressControl().
        type("s").releaseControl());
windows.pause(PAUSE);

Probably the simplest way is to enter the full path to the folder in which we want to save the file, including its name and extension, in the text field "Name".

Setting the focus by pressing Alt + N

1
2
3
4
5
6
/*
 * Find the control "Name" sending the sequence Alt+N to obtain the
 * focus.
 */
windows.typeText(windows.getKeyboardSequence().pressAlt().
        type("n").releaseAlt());

Typing the full path including the file name

1
2
3
4
// Build the whole path to the file
String file = "wordpad_example.rtf";
String path = Paths.get(server.getCurrentDir(), file).
        toFile().getAbsolutePath();

Now we can type the path in the field "Name".

1
2
// Write the whole path to the file
windows.typeText(path);

Pressing the button "Save As", through Alt + S

1
2
3
4
// Save the file by pressing Alt+S to open the dialog
windows.typeText(windows.getKeyboardSequence().pressAlt().
        type("s").releaseAlt());
windows.pause(PAUSE);

Like in the Notepad example, we should check whether the application is waiting for our confirmation to replace the file.

1
2
3
4
5
6
7
8
9
10
/*
 * If the file already exists, replace it. To know that the dialog is
 * active when the active window is not Wordpad.
 */
if (!windows.getActiveWindowTitle().
        contains("wordpad_example.rtf")) {
    // Press Yes
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("y").releaseAlt());
}

Typing formatted text on WordPad

In the two previous examples, we have left both Notepad and WordPad opened.

Now we are going to type some text in the WordPad file example_wordpad.rtf.

First, we are activating and maximizing the WordPad windows. Then we are typing some text lines and applying different formats.

Activating and maximizing WordPad

1
2
3
4
5
6
// Obtain the HWND for WordPad
HWND hWordpad = windows.getWindow("wordpad_example.*").
        gethWnd();
// Activate and maximize WordPad
windows.showWindow(hWordpad, EShowWindowState.SW_SHOWMAXIMIZED);
windows.pause(PAUSE);

For clarity, we have separated the WordPad HWND collection. The method show can receive different parameters EShowWindowState, and in this case, it receives EShowWindowState.SW_SHOWMAXIMIZED, which will activate and maximize the window referenced by the HWND hWordpad

Typing text and making it bold

Let's type the text "This is the first line".

1
2
// Write "This is the first line"
windows.typeText("This is the first line");

Next, we are pressing Return to go to a new line.

1
2
// Press Return to point the cursor to the next line
windows.getKeyboardSequence().typeReturn().apply();

Here we have used another way to apply an IKeyboardSequence.

To boldface the text, we should place the cursor at the beginning of the line. As usual, we have several choices, but pressing the UP key seems to be the easiest one. Additionally, since it is the first line, we could press the combination Ctrl + Home, which would take us to the beginning of the first text line.

1
2
// Press the up arrow to go to the beginning of the first line
windows.getKeyboardSequence().up();

Line selection

It's time to select the line. Again, we have different choices. We dismiss using Ctrl + A (Select All) which, though it would work in this case with one single line, it is not what we are looking for, because it would select all the text in the document. Another option would be pressing Ctrl + Shift + Left to select the line word by word, which would be also a valid option because we know exactly how many words we want to select, but we are looking for a method that works regardless of the number of words in the line. We can also use Ctrl + Shift + Down, which would select the whole line.

client-wordpad-01.png

This last option (Ctrl + Shift + Down) seems to be the one we are searching. However, we are using the mouse, which is not the easiest method, but it will help us go on learning new skills. The point is that, if we can place the mouse pointer at the beginning of the line, at the small area between the window's left border and the beginning of the top ruler, we can select the line by clicking once. We have represented this small area with a blue box in the previous image.

Note: It is very important to bear in mind that using the mouse to put the cursor on the screen depends on the resource's screen resolution. For this reason, we should always use the information about the coordinates provided by the agent, since they will change every time the resolution changes.

We need to determine the position on which we want to put the mouse pointer, and to do so we are going to use the tool "Item Inspector". We can see that the x coordinate of the point on which we should place the pointer is between 6 and 20 approximately, relative to the window. This information is available in the Item Inspector. You should always use coordinates relative to the window, instead of the screen, since the window may not be aligned with the screen. For example, the window could be moved, and so it would be better to get the position of the object Rectangle of the window and use that position to add the values relative to the window. This will work even though the window has moved.

Remember that the position of the object Rectangle refers to the screen coordinate on which the window's upper left-hand corner is.

client-window.png

In the above image, you can see that the absolute positions of points a and b on the screen are different, although their windows relative positions are identical. We can consider the windows A and B as representing the same window in different executions that, for any reason, has been moved on the screen.

However, knowing their window relative position and their position on the screen relative to its upper left-hand corner, we can get their coordinates regardless of the position of the window. With this in mind, we already know that the blue area in the previous image covers values for x from 6 to 20. A value of 14 (for example) looks safe for x, so we have x = 14 inside the blue area.

Now let's determine the value of the y coordinate for the first line. Again, through the Appian RPA agent's Item Inspector, we can see that the value of the y coordinate for the first line ranges from 143 to 158 approximately (this detail may vary a little from one machine to another). We can assume that the line is 15 pixels high, therefore a safe value for y could be 150-151 pixels.

So, by clicking on the point with coordinates (14, 150) our robotic process will select the first line.

As mentioned before, there are easier options, and for the second and subsequent lines we must adjust these values, but the example will illustrate how to click on a specific point, and how to distinguish an absolute point from a window relative point. Additionally, in this case we have maximized the window, so we can be sure of its location. Even so, let's go.

Auxiliary variables

1
2
3
4
5
6
// Define auxiliary variables
int lineHeight = 15; // line height
int lineNumber = 1; // number of the line
int xLine = 1030; // x position relative to the window
// relative position to the upper edge of the first line
int yLine = 276;

We have defined four variables:

  • lineHeight: this variable represents the line's height that we have calculated through the Item Inspector.
  • lineNumber: this is the line indicator, in our case it is always 1.
  • xLine: this is the window relative x position on which we want to click. It won't change, regardless of the line we want to select.
  • yLine: this is the window relative position for the line top border. We will us this value to calculate the value of the y coordinate depending on the line to be selected.

Determining the WordPad window's position

Next step will be to determine the WordPad window's position, regardless of whether we know its location or not, that is, programmatically.

We assume that the window is displayed in the foreground. You can know this by getting the object Rectangle of the window.

1
2
3
// Obtain the Rectangle of WordPad
Rectangle rect = windows.getWindow("wordpad_example.*").
        getRectangle();

Now we will get the property Location of the Rectangle of WordPad, that is, an object Point that points at the window's upper left-hand corner. This value can be obtained regardless of whether we know the window's position or not, or whether it's maximized or not.

1
2
// Obtain Location (upper left corner)
Point pWordpad = rect.getLocation();

Next, we adjust the value of pWordpad to represent the point on which we want to click (approximately 1030, 276) always relative to the Wordpad window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
 * The corresponding values for x and y must added to those from
 * pWordpad.
 */

/*
 * x: this a fixed value, obtained regardless of the Wordpad position.
 */
pWordpad.x += xLine;

/*
 * y: this value must be calculated from:
 * - The 'y' position of the first line (upper edge).
 * - Adding (lineNumber * lineHeight) we get the bottom edge.
 * - Finally, when substracting (lineHeight / 2) we get the central point.
 */
pWordpad.y += yLine + (lineNumber * lineHeight) - (lineHeight / 2);

Suppose the WordPad window coincides with the absolute (0, 0) point. We should add 1030 to the x value and the y value would be:

1
y = 276 + (1 * 15) - (15 / 2) → 276 + 15 -7.5 (redondeado a 7) → 276 + 8 → y = 284

Now suppose the WordPad window is located at any other point on the screen, for example (50, 100). The previous code will adjust the value of the desired coordinate:

1
2
 x = 50 + 1030 = 1080
 y = 100 + 276 + (1 * 15) - (15 / 2) → 100 + 276 + 15 -7.5 (redondeado a 7) → y = 394 

Clicking on the selected point

We only have to click on the selected point:

1
2
// Now, it is possible to do a click on the calculated point
windows.mouseLeftClick(pWordpad);

As mentioned before there are many easier ways of performing this task, however, this was a good way of introducing the API for windows and mouse handling.

Make the selected text bold

We want to format the text and boldface it. As usual, we have different choices. We can determine the window relative position of the button "Bold B" and click on it, like we did in the previous step. However, the best choice is to press Ctrl + B.

1
2
3
// Put the selected text in bold with Ctrl+B
windows.typeText(windows.getKeyboardSequence().pressControl().
        type("b").releaseControl());

Changing the text to Verdana font

Let's choose the Verdana font that will be applied to the text. By default, the selected font is Calibri.

We should access the WordPad ribbon to choose the font by accessing the corresponding field. Let's have a look at the "Font" ribbon:

client-wordpad-02.png

We can see that the controls we need to change the font are in the second block of the ribbon. It looks a logical choice to type the text "Verdana" in the text field "Font", just where "Calibri" is shown.

We have several choices to access the text field. In this example we will use the method IField getField(Point location) of the interface IWindows, which returns an object IField that represents a text field, whose value can be set and got. We need to know the control's position, which as the Appian RPA Item Inspector shows, is (230, 110). This position is, as usual, relative to the window (you should check this value on each machine).

As usual, we will obtain the absolute position from the dialog box window's position.

1
2
3
4
5
6
7
8
9
10
11
// Obtain the field font type
// Rectangle of the windows
Rectangle rectFontWindow;
rectFontWindow = windows.getWindow(
        windows.getActiveWindowTitle()).getRectangle();
// Absolute position of the rectangle
Point pFontWindow = rectFontWindow.getLocation();
// Absolute position of the control
Point pFieldFont = new Point(
        pFontWindow.x + 230,
        pFontWindow.y + 110);

First, we get the object Rectangle corresponding to the dialog box, to obtain its absolute position. From this position, we obtain the control's absolute position. Once we know the position of the control we are searching, we can use the method getField(Point location).

1
2
// Obtain the text field
IField fieldFont = windows.getField(pFieldFont);

To select Verdana among the fonts available, we can type "Verd" in the text field, or even "Verdana". Since WordPad will try to help us select an existing font, we will have to click on the field, press the Down arrow key, type in the value and, finally, press Return.

1
2
3
4
5
6
7
8
// Type "Verd" to select "Verdana"
if (fieldFont != null) {
    fieldFont.click();
    windows.typeText(windows.getKeyboardSequence().down());
    fieldFont.setValue("Verd", true);
    windows.typeText(
            windows.getKeyboardSequence().typeReturn());
}

We first check that the object fieldFont is not null, and use the method setValue(String value, boolean clean) to set the text. This method is overloaded. Here, before typing the text, we erase the existing text.

The other choice, void setValue(String value), is equivalent, but it does not erase the existing text in the field text. It would be the same as using setValue(text, false).

Increasing font size to 42

Another way of accessing a field is by navigating through a keystroke sequence. In our case, to change the font size we can navigate to the corresponding field by using the key sequence Alt, H, S and 1. We can see this navigation on the ribbon, with the suggested key sequence highlighted in the following image:

client-wordpad-3.png

Once we have reached that field, we type "42" and press Return.

1
2
3
4
5
6
7
// Increase the font size
// Navigation to the field Tamaño (Size): Alt, H, S, 1
windows.typeText(windows.getKeyboardSequence().typeAlt().
        type("h").type("s").type("1")); 
// Type "42"
windows.typeText("42");
windows.typeText(windows.getKeyboardSequence().typeReturn());

Centering the text

Next, we are going to center the text. We could do this by simply pressing Ctrl + E, but let's make it more interesting and learn more Appian RPA techniques.

On Windows environments, everything is a window: a button is a window, a field text, a checkbox or a combo box. We are going to obtain and handle the combo to center the text from the control "Paragraph", accessing it through the context menu. To do so, we need to right click and access the option "Paragraph" by pressing on the "A" key.

1
2
3
4
5
// Align the text centered
// Select Paragraph using the contextual menu
windows.mouseRightClick(pWordpad);
windows.typeText(windows.getKeyboardSequence().type("a"));
windows.typeText(windows.getKeyboardSequence().typeReturn());

Once in the window "Paragraph", we will access the align combo through the keystroke Alt + A twice, since we cannot obtain its WindowInfo because its name does not correspond with the combo, but with its label.

1
2
3
4
5
6
7
8
9
/*
 * Access to the combo labeled as "Alignment:" using Alt+A.
 * Although the control "Alignment" is only associated to the label,
 * the focus will be obtained as the combo.
 */
windows.typeText(windows.getKeyboardSequence().pressAlt().
type("a").releaseAlt());
windows.typeText(windows.getKeyboardSequence().pressAlt().
type("a").releaseAlt());

Once we have put the focus on the combo, we can press the "c" key to select the option "Center".

Line spacing

Once we have centered the text, we want to apply a line spacing of 2, which means that the next line will be at a distance equivalent to two lines. We won't leave the window "Paragraph" to accomplish this.

We will access the control "Line Spacing" by pressing Alt + S.

1
2
3
// Press Alt+S to access to the control 'Line Spacing'
windows.typeText(windows.getKeyboardSequence().pressAlt().
        type("s").releaseAlt());

To choose a line spacing of size 2, we are going to obtain the control ComboBox, or rather, an object ICombo which represents a ComboBox from its HWND.

To do so, we will obtain a list of objects WindowInfo for the child controls (windows) of the window "Paragraph".

1
2
3
// Obtain the controls of the active window
List<WindowInfo> list = windows.enumChildWindows(
        windows.getForegroundWindow());

If everything is ok, the list will contain the WindowInfo for our combo; otherwise, the list will be empty.

We will search the information we want in the list of window controls stored within list. Let's look for our ComboBox.

1
2
3
4
5
6
7
8
9
10
11
// Obtain the ComboBox of 'Interlineado' from 'list'
Stream<WindowInfo> stream = list.stream().filter(
        ctrl -> ctrl.getClassName().equals("ComboBox") &&
        "1,15".equals(windows.getCombo(ctrl.gethWnd()).getValue()));
ICombo spacingCombo = 
        windows.getCombo(stream.findFirst().get().gethWnd());
if (spacingCombo == null) {
    windows.typeText(windows.getKeyboardSequence().down(2));
} else {
    spacingCombo.setValue("2,00");
}

We have used the method list.stream(), which is new in Java 8, and we have applied a filter to keep only those WindowInfo whose class is ComboBox and which have the value "1,15" selected.

Once we have found the right WindowInfo, we can exit the loop.

We should distinguish the class ComboLBox, which is the ComboBox of a ListItem. The method WindowInfo getClassName() returns the control's class.

Once filtered, our stream should only contain the ComboBox we are looking for. This is valid for this example. If we need to change the line spacing several times, we should find another way to unambiguously determine that it is the right ComboBox.

If we have found the ComboBox, then the object spacingCombo is not null, and we will be able to establish the selected item. But remember that we must make our robot reliable, so we include an alternative. In this case, we select the Down arrow key twice until reaching "2,00".

1
2
// Type Return to exit and apply the changes to the paragraph
Windows.typeText(windows.getKeyboardSequence().typeReturn());

Copy and paste text

In the previous examples, we have opened an existing file with the Notepad, notepad_example.txt, we have created a file with WordPad, we have saved it and we have entered some text that subsequently have been boldfaced. We haven't closed these files intentionally, because we want all these examples to make an example robot together. Now, we are going to copy the text we typed in WordPad ("This is the first line") to paste it on the Notepad.

This involves selecting the text, copying it, activating the Notepad window, and pasting it.

When we finished the previous step, the WordPad window was in the foreground, with the text "This is the first line" selected and boldfaced.

Obtaining the handlers HWND for Notepad and WordPad

The first thing we need are the handlers of both windows:

1
2
3
4
5
// Obtain the HWND of Wordpad and Notepad
HWND hWndWordpad =
        windows.getWindow("example_wordpad.*").gethWnd();
HWND hWndNotepad =
        windows.getWindow("example_notepad.*").gethWnd();

We must do this to be able to operate the windows: minimizing them, activating them, maximizing them, etc.

Copying the desired text

Let's copy and get the previously selected text:

1
2
// Copy and paste the text
String text = windows.copyAndGet();

This code snippet accesses the clipboard and returns null if it is not available or empty. Another method we can use is robot.cleanCopyAndGet(robot), which previously erases the clipboard and then copies and gets the selected text.

Pasting the copied text on Notepad

First, we check if the variable text is null. If everything went well and we have properly copied the text, we will paste it on the Notepad; otherwise we will warn that a problem has occurred.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// If focus
if (text == null || text.isEmpty()) {
    // Type text
    windows.typeText("Could not copy the text");
    server.setCurrentItemResultToWarn();
} else {
    // Deactivate Wordpad
    windows.showWindow(hWndWordpad,
            EShowWindowState.SW_MINIMIZE);
    windows.pause(PAUSE);
    // Activate and maximize Notepad
    windows.showWindow(hWndNotepad, EShowWindowState.SW_SHOWMAXIMIZED);
    windows.pause(PAUSE);
    // Type the text
    windows.typeText(text);
    server.setCurrentItemResultToOK();
}

We check that the text we want to paste on Notepad has been obtained. If so, we minimize the WordPad window by using the method showWindow through its HWND and EShowWindowState.SW_MINIMIZE. Remember that this option minimizes the window that is in the foreground and activates the next window. Then, we make a pause to let this task complete.

Next, we show the Notepad window in the same way, through its HWND, and this time, with EShowWindowState.SW_SHOWMAXIMIZED, which activates and shows the Notepad window on the current position and with the current size. Once we have Notepad in the foreground, we just have to type the text stored in the variable text.

If the variable text is null, we will type a text that will warn that a problem has occurred when copying and getting the text.

Closing the windows

Now we can close both windows. In fact, it is advisable and a good practice to leave the machine in the same status as it was before the execution. In this case, this means that if our robot has opened Notepad and WordPad, it should leave both windows closed.

When we close the windows, we expect them to ask us to save the changes made on the files. In this case, we will choose not to save them.

First, we will obtain the objects HWND for Notepad and WordPad.

Obtaining the objects HWND

1
2
3
4
HWND hWndWordpad =
        windows.getWindow("wordpad_example.*").gethWnd();
HWND hWndNotepad =
        windows.getWindow("notepad_example.*").gethWnd();

Closing Notepad through Alt+F4 without saving

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Use Alt+F4 to close the files
if (hWndNotepad != null) {
    windows.showWindow(hWndNotepad,
            EShowWindowState.SW_SHOWMAXIMIZED);
    windows.typeText(windows.getKeyboardSequence().typeAltF(4));
    windows.pause(PAUSE);
    // If a dialog asks for saving the changes
    if (windows.getActiveWindowTitle().equals("Notepad")) {
        /*
         * Choose no, pressing "No". Obtain HWND of the confirmation
         * window.
         */
        HWND hWndConfirmationWindow =
                windows.getForegroundWindow();
        if (!windows.clickButton(
                windows.getControl(
                        hWndConfirmationWindow, 7))) {
            // Press button with Alt+N if failed
            windows.typeText(windows.getKeyboardSequence().
                    pressAlt().type("n").releaseAlt());
        }
    }
}

First, we check that the HWND for the Notepad window is not null, that is, the window is still opened. We activate it and maximize it to be sure that we are working on the window, and then we press Alt+F4 through the method typeAltF(4) which we already know.

We should check if the application is asking us if we want to save the changes. To do so, we need to know if the active window's title is "Notepad." We don't use a regular expression as we did in previous cases. The title should be "Notepad" because this is the title of the dialog box window that would be waiting for our confirmation. In other words, when the window that requests our confirmation to save the changes appears, it becomes the active window.

If the condition evaluates as true, we will choose NOT to save the changes, so we should press the button No (following image).

client-notepad-1.png

As usual, we have several choices, for example Alt + N. But in this case, we will click on the button by using the method:

1
boolean clickButton(HWND hWndButton);

This method receives the HWND for the button we want to press as a parameter, so we need to know its identifier and the HWND of the window it belongs to.

It will return true is everything has gone well and the button has been pressed. The HWND for the window will be obtained in the following line:

1
HWND hWndConfirmationWindow = window.getForegroundWindow();

And the identifier will be obtained through the Appian RPA client's "Item Inspector". In this case, it is 7.

We obtain the HWND for the button with:

1
windows.getControl(    hWndConfirmationWindow, 7)))

And we check that the button has been pressed:

1
if (!windows.clickButton(windows.getControl(hWndConfirmationWindow, 7))) {

To make sure that the button is pressed, if the method clickButton returns false, we will press Alt + N.

Closing WordPad through Alt+F4 without saving

We will close WordPad controlling the confirmation popup to save changes, in this case closing the windows using Alt + F4 and Alt + N to say we don't want to save changes.

1
2
3
4
5
6
7
8
9
10
11
12
if (hWndWordpad != null) {
    windows.showWindow(hWndWordpad,
            EShowWindowState.SW_SHOWMAXIMIZED);
    windows.typeText(windows.getKeyboardSequence().typeAltF(4));
    windows.pause(PAUSE);
    // If a dialog asks if we want to save the changes
    if (windows.getActiveWindowTitle().equals("WordPad")) {
        // Choose no
        windows.typeText(windows.getKeyboardSequence().
                pressAlt().type("n").releaseAlt());
    }
}

Showing a message or warning on screen

Finally, we will show an example of how to display a message to the user on screen. This is an important circumstance, because it involves making the robot wait until the user provides an answer by selecting one of the choices.

This kind of messages is quite unusual in Appian RPA robots' execution, because its use would force the user to perform an action. However, for those cases in which this kind of interaction is needed, Appian RPA provides a human assisted execution system, through the methods sendQuestion and getAnswer from the interface IJidokaServer<?> / IJidokaContext<?>.

1
2
3
4
// Show a message to notify the end of the execution
windows.messageBox(windows.getForegroundWindow(),
    "End of processing reached, the robot will wait until an answer to this message", "End",
    EMessageBox.MB_OK, EMessageBox.MB_ICONINFORMATION);

The method messageBox(HWND owner, String text, String title, EMessageBox... types) allows you to show a warning or informational message on screen.

The robot's execution will remain in standby until the user answers the message.

This method returns an object of type EMessageBoxResult, which has not been evaluated in this example, and which identifies the action performed by the user as an answer to the message.

Its possible values are:

  • IDABORT: the user has pressed "Abort".
  • IDCANCEL: the user has pressed "Cancel".
  • IDCONTINUE: the user has pressed "Continue".
  • IDIGNORE: the user has pressed "Ignore".
  • IDNO: the user has pressed "No".
  • IDOK: the user has pressed "OK".
  • IDRETRY: the user has pressed "Retry".
  • IDTRYAGAIN: the user has pressed "Try again".
  • IDYES: the user has pressed "Yes".

The user will choose one choice or another depending on the choices available in the message. These choices, or FLAGS, and others are specified in the method messageBox, which receives the following parameters:

  • HWND owner: the window that owns the message. We have used window.getForegroundWindow(), which in this case is equivalent to the Desktop, because there was no foreground window (since we have closed WordPad and Notepad).
  • String text: this is the text that will appear in the message.
  • String title: 'this is the window's title to be shown.
  • EMessageBox... types: this parameter is an array of objects EMessageBox, which basically are the options for building a message. The possible values of EMessageBox are:
    • MB_ABORTRETRYIGNORE: the window will show three buttons: Abort, Retry and Ignore.
    • MB_CANCELTRYCONTINUE: the window will show three buttons: Cancel, Try again and Continue. This method is an alternative to the previous one.
    • MB_HELP: a button Help is added to the message window.
    • MB_OK: default option. The window shows only one button OK.
    • MB_OKCANCEL: the window shows two buttons, OK and Cancel.
    • MB_RETRYCANCEL: the window shows two buttons: Retry and Cancel.
    • MB_YESNO: the window shows the buttons Yes and No.
    • MB_YESNOCANCEL: the window shows the buttons Yes, No and Cancel.
    • MB_ICONEXCLAMATION, MB_ICONWARNING: an exclamation icon is shown.
    • MB_ICONINFORMATION, MB_ICONASTERISK: an information icon is shown.
    • MB_ICONQUESTION: a question mark icon is shown. This icon is not recommended because its use may lead to confusion, since it could be seen as help. It's still included for compatibility reasons.
    • MB_ICONSTOP, MB_ICONERROR, MB_ICONHAND: these three options show an icon of a white "X" in a red circle.
    • MB_DEFBUTTON1: the first button is the default button, unless another one has been specified through MB_DEFBUTTON2, MB_DEFBUTTON3 or MB_DEFBUTTON4.
    • MB_DEFBUTTON2: the second button is the default button.
    • MB_DEFBUTTON3: the third button is the default button.
    • MB_DEFBUTTON4: the fourth button is the default button.
    • MB_APPLMODAL: the user must answer the message to continue working on the window specified in the parameter HWND owner. We can work on other windows of other threads. Within the same thread, we can move to other windows depending on the windows hierarchy. All the child windows of the window specified by owner will be deactivated, except the pop-up windows. This is the default option if we have specified neither MB_SYSTEMMODAL nor MB_TASKMODAL
    • MB_SYSTEMMODAL: like the previous one, but this option makes the window always visible. This option gives the message the style WS_EX_TOPMOST and the message will keep on top of all the windows which don't have the style WS_EXT_TOPMOST. You can use this option for severe error or warning notifications, which could damage the system thus requiring his or her immediate attention.
    • MB_TASKMODAL: like MB_APPLMODAL but in this case all windows in first level belonging to the thread will be deactivated if the parameter owner is null.
    • MB_DEFAULT_DESKTOP_ONLY: the message will be shown by default on the desktop.
    • MB_RIGHT: the text is right-justified.
    • MB_RTLREADING: sets the message to be read from right to left on Hebrew and Arabic systems.
    • MB_SETFOREGROUND: the message appears in the foreground.
    • MB_TOPMOST: the message is created with the style WS_EX_TOPMOST.
    • MB_SERVICE_NOTIFICATION: this is a message thrown by a service to notify some event. It will be shown in the current desktop even though no user is logged. HWND owner must be null.

Read more about the MessageBox function.

Example robotic process

We are going to create a robot with all these examples, step by step.

Source Code

You can get the source code of the robot from the following link:

Description Link
Source Code robot-developmentguide-client.zip

Workflow

Our robot's workflow will consist of 8 actions, as shown in the following picture.

We will build the workflow through the Appian RPA Console, so we should know the list of actions, the interaction between them and the name of the methods that will implement them.

The following table shows the list of actions and the name of their associated methods:

Action Method
Init init
Create Notepad File createNotepadFile
Create WordPad File createWordpadFile
Insert Text in WordPad insertTextInWordpad
Format text formatText
Copy & Paste copyAndPaste
Close Files closeFiles

In the following illustration, you can see that our workflow is quite simple. It has no conditional action, because we think that the important thing to learn with this robot is how to use the Client module's methods and features.

client-workflow-1.png

Before saving the workflow, you should associate the names of the methods defined in the previous table, which will be later implemented in the robot's code.

Implementation

We have called our class ClientRobot, which as you know should implement the interface IRobot and be preceded by the annotation @Robot.

1
2
@Robot
public class ClientRobot implements IRobot

Next, we will declare the attributes of type IWindows and IJidokaServer<?>:

1
2
3
4
5
6
7
8
9
/**
 * Server
 */
private IJidokaServer<?> server;

/**
 * Windows module.
 */
private IWindows windows;

The next step is to declare several attributes that we need.

We will use the constant PAUSE to specify the duration of the pauses that our robots will make, because sometimes it will be necessary to adapt its working pace.

1
2
3
4
/**
 * Pause between actions to mimic human behavior.
 */
private static final int PAUSE = 1000;

The array items will contain the list of items to be processed. In this robot, we have considered an item as a task to be performed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Items list to process.
 */
private String[] items = new String[]{
        "Create notepad_example.txt file with Notepad",
        "Create wordpad_example.rtf file with WordPad",
        "Insert texte in WordPad",
        "Put text in bold",
        "Change font to Verdana",
        "Change font size to 42",
        "Center texto",
        "Add spacing",
        "Copy text and paste it into Notepad",
        "Close unsaved files"
};

There are a couple of variables that will serve us as counters:

  • currentItem: which will be used to inform the server about the item currently being processed.
  • itemIndex: which will be used to read the item within the array items.

It should be noted that, although a single variable would have been enough, for greater clarity we will use two variables: itemIndex, zero-based, and currentItem, one-based. The last one will be used in the API for statistics.

1
2
3
4
5
6
7
8
9
/**
 * Current item counter.
 */
private int currentItemIndex = 1;

/**
 * Current item index in the items array.
 */
private int itemIndex = 0;

The starting method, as we have defined it in the workflow through the console, will be init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Init Action.
 * 
 * @throws Exception
 */
public void init() throws Exception {
    
    // Initialize server
    server = (IJidokaServer<?>) JidokaFactory.getServer();
    // Initialize robot
    windows = IJidokaRobot.getInstance(this);
    // Specify pauses
    windows.typingPause(PAUSE);
    windows.mousePause(PAUSE);      
    // Notify the server the number of items to process
    server.setNumberOfItems(items.length);
}

This method represents the first action of the workflow. We can see that it meets the required criteria: it is public, returns nothing, and receives no parameters.

We initialize the objects server and robot in as we know. In this robot, we are not using any context class, to focus on Client module's methods.

We could have initialized the object robot like this:

1
windows = IWindows.getInstance(this);

The result is the same, since IJidokaRobot knows what class should be returned by its method getInstance(this).

We specify the pauses that our robot should make while using the keyboard and the mouse. And finally, we inform the server about the number of items to be processed.

The next action, "Create Notepad File", has the method createNotepadFile associated.

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
/**
 * Action "Create Notepad File".
 * 
 * @throws Exception
 */
public void createNotepadFile() throws Exception {
    
    // First item processing start
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    // Log text
    server.info(items[itemIndex]);
    // Windows + R
    windows.typeText(windows.getKeyboardSequence().pressWindows().
            type("r").releaseWindows().pause());
    // Write 'notepad'
    windows.typeText("notepad");
    // Type Return
    windows.typeText(windows.getKeyboardSequence().typeReturn());
    // Fixed pause to let Notepad open
    windows.pause(PAUSE);
    
    // Save the file by pressing Ctrl+G to open dialog
    windows.typeText(windows.getKeyboardSequence().pressControl().
            type("g").releaseControl());
    windows.pause(PAUSE);

    /*
     * Now, we must find the control "Nombre" by pressing Alt+O we obtain
     * the focus.
     */
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("o").releaseAlt());
    
    // Build the whole path to the file
    String file = "notepad_example.txt"; 
    String path = Paths.get(server.getCurrentDir(), file).
            toFile().getAbsolutePath();

    // Write the file path 
    windows.typeText(path);
    // Type Return
    windows.typeText(windows.getKeyboardSequence().typeReturn()); 
    windows.pause(PAUSE );

    /*
     * If the file already exists, it is replaced.
     * To know when the question is shown, we must check that the active
     * window is not Notepad.
     */
    if (!windows.getActiveWindowTitle().contains("notepad_example")) {
        // Press Sí (yes)
        windows.typeText(windows.getKeyboardSequence().pressAlt().
                type("s").releaseAlt());
    }
    
    // Notify the end of the item processing and the result
    server.setCurrentItemResultToOK();
    
    // Increase counters
    itemIndex++;
    currentItemIndex++;
}

We have already seen this method's code. We have just added the code to inform the server about the beginning of the first item processing, and once finished, about its result. We increase the counters as well:

1
2
3
4
server.setCurrentItem(currentItemIndex, items[itemIndex]);
server.setCurrentItemResultToOK();
itemIndex++;
currentItemIndex++;

Let's go on with the implementation of the next action, which corresponds to the method createWordpadFile, as defined through the console.

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
public void createWordpadFile() throws Exception {
    // Starting the second item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    // Insert text in the log
    server.info("Creating the example file wordpad_example.rtf");
    // Open the dialog box to run win Win + R
    windows.typeText(windows.getKeyboardSequence().pressWindows().
            type("r").releaseWindows());
    windows.pause(PAUSE);
    // Type wordpad
    windows.typeText("wordpad");
    // Press Return to open Wordpad
    windows.typeText(windows.getKeyboardSequence().typeReturn());
    // Wait until WordPad is opened
    windows.waitCondition(10, 3000, "WordPad opened", null, true,
(i,context) -> windows.getWindow("Document - WordPad.*") != null);

    // Save the file by pressing Ctrl+G to open the dialog
    windows.typeText(windows.getKeyboardSequence().pressControl().
            type("g").releaseControl());
    windows.pause(PAUSE);
    /*
     * Find the control "Nombre" sending the sequence Alt+O to obtain the
     * focus.
     */
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("o").releaseAlt());
    // Build the whole path to the file
    String file = "wordpad_example.rtf";
    String path = Paths.get(server.getCurrentDir(), file).
            toFile().getAbsolutePath();
    // Write the whole path to the file
    windows.typeText(path);
    // Save the file by pressing Alt+G to open the dialog
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("g").releaseAlt());
    windows.pause(PAUSE);
    /*
     * If the file already exists, replace it. To know that the dia-log is
     * active when the active window is not Wordpad.
     */
    if (!windows.getActiveWindowTitle().
            contains("wordpad_example.rtf")) {
        // Press Yes (Sí)
        windows.typeText(windows.getKeyboardSequence().pressAlt().
                type("s").releaseAlt());
    }
    // Wait until back to WordPad main.
    windows.waitCondition(10, 3000, "WordPad opened", null, true, 
            (i,context) -> 
                windows.getWindow("wordpad_example.*") != null);

    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;
}

Like the previous method, this code is the same as some of this section's examples we have already seen. We have just added the information that is sent to the server.

The next action corresponds to the method insertTextInWordpad, which will type some text on WordPad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void insertTextInWordpad() throws Exception {
    
    // Start of the third item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    
    // Obtain the HWND for WordPad
    HWND hWordpad = windows.getWindow("wordpad_example.*").
            gethWnd();
    // Activate and maximize WordPad
    windows.showWindow(hWordpad, EShowWindowState.SW_SHOWMAXIMIZED);
    windows.pause(PAUSE);
    
    // Write "This is the first line"
    windows.typeText("This is the first line");
    // Press Return to point the cursor to the next line
    windows.getKeyboardSequence().typeReturn().apply();
    // Press the up arrow to go to the beginning of the first line
    windows.getKeyboardSequence().up();
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;
}

The next method is slightly larger, and covers several items processing, all related to text formatting.

We have already seen its code as well in previous examples, but we will show it here separated by items, due to its extension.

The code of the first item processing will select and boldface the text:

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
public void formatText() throws Exception {
    
    // Start of the fourth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    
    // Define auxiliary variables
    int lineHeight = 15; // line height
    int lineNumber = 1; // number of the line
    int xLine = 360; // x position relative to the window
    // relative position to the upper edge of the first line
    int yLine = 185;
    
    // Obtain the Rectangle of WordPad
    Rectangle rect = windows.getWindow("wordpad_example.*").
            getRectangle();
    // Obtain Location (upper left corner)
    Point pWordpad = rect.getLocation();

    /*
     * The corresponding values for x and y must added to those from
     * pWordpad.
     */
    
    /*
     * x: this a fixed value, obtained regardless of the Wordpad po-sition.
     */
    pWordpad.x += xLine;

    /*
     * y: this value must be calculated from:
     * - The 'y' position of the first line (upper edge).
     * - Adding (lineNumber * lineHeight) we get the bottom edge.
     * - Finally, when subtracting (lineHeight / 2) we get the cen-tral
     * point.
     */
    pWordpad.y += yLine + (lineNumber * lineHeight) - (lineHeight / 2);
    
    // Now, it is possible to do a click on the calculated point
    windows.mouseLeftClick(pWordpad);
    
    // Put the selected text in bold with Ctrl+N
    windows.typeText(windows.getKeyboardSequence().pressControl().
            type("n").releaseControl());
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;
    

In the next item, we search for the text field that will allow us to set the font. We will get it through the interface IField. To do so, we calculate the control's coordinates, and once we have obtained it, we type the text.

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
   // Start of the fifth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    // Obtain the field font type
    // Rectangle of the windows
    Rectangle rectFontWindow;
    rectFontWindow = windows.getWindow(
            windows.getActiveWindowTitle()).getRectangle();
    // Absolute position of the rectangle
    Point pFontWindow = rectFontWindow.getLocation();
    // Absolute position of the control
    Point pFieldFont = new Point(
            pFontWindow.x + 230,
            pFontWindow.y + 80);

    // Obtain the text field
    IField fieldFont = windows.getField(pFieldFont);

    // Type "Verd" to select "Verdana"
    if (fieldFont != null) {
        fieldFont.click();
        windows.typeText(windows.getKeyboardSequence().down());
        fieldFont.setValue("Verd", true);
        windows.typeText(
                windows.getKeyboardSequence().typeReturn());
    }
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;

The next step is trivial: we enter the keystroke sequence to navigate to the control that allows us to change the font size.

1
2
3
4
5
6
7
8
9
10
11
12
13
   // Start of the sixth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    // Increase the font size
    // Navigation to the field Tamaño (Size): Alt, H, S, 1
    windows.typeText(windows.getKeyboardSequence().typeAlt().
            type("h").type("s").type("1")); 
    // Type "42"
    windows.typeText("42");
    windows.typeText(windows.getKeyboardSequence().typeReturn());
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;

Now it's time to open the context menu over the text to center it. So, we need to right-click on the text and choose the proper option to open the window "Paragraph". Then, bearing in mind that the controls can be activated through the combinations Alt + letter (they will carry the character '&' before the letter), we will access the control "Alignment", using 'L' in this case (since the control's text is "A&lignment:").

By doing this, we will have the focus on the combo for text alignment, though this is a different control from the one we have accessed, which can be seen through the tool "Item Inspector". In this combo, we will select the centered text by pressing the letter "c".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   // Start of the seventh item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    
    // Align the text centered
    // Select 'Párrafo' (Paragraph) using the contextual menu
    windows.mouseRightClick(pWordpad);
    windows.typeText(windows.getKeyboardSequence().type("p", 2));
    windows.typeText(windows.getKeyboardSequence().typeReturn());
    
    /*
     * Access to the combo labeled as "Alineación:" using Alt+L.
     * Although the control "A&lineación:" is only associated to the
     * label, the focus will be obtained as the combo.
     */
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("l").releaseAlt());
    // Select 'Centro' (Center).
    windows.typeText("c");
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;
    

For the next item, to change the paragraph line spacing to "2", we use the interface ICombo to obtain the ComboBox that allows you to select the line spacing.

We get a stream from the list of objects WindowInfo looking for the ComboBox. To do so, we filter stream by the class name "ComboBox" and the text of the item that is selected in the control.

It should be borne in mind that, sometimes, we can find objects ICombo which, being not null, may return null when we try to get their value through getValue().

Once we have found the ComboBox that we were looking for, we can set its value through setValue("2.00"), including as usual an alternative, in case an error occurs. In this case, we press the Down key twice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   // Start of the eighth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    // Press Alt+N to access to the control 'Interlineado' (Spacing)
    windows.typeText(windows.getKeyboardSequence().pressAlt().
            type("n").releaseAlt());
    
    // Obtain the controls of the active window
    List<WindowInfo> list = windows.enumChildWindows(
            windows.getForegroundWindow());
    // Obtain the ComboBox of 'Interlineado' from 'list'
    Stream<WindowInfo> stream = list.stream().filter(
        ctrl -> ctrl.getClassName().equals("ComboBox") &&
        "1,15".equals(windows.getCombo(ctrl.gethWnd()).getValue()));
    ICombo spacingCombo = 
            windows.getCombo(stream.findFirst().get().gethWnd());
    if (spacingCombo == null) {
        windows.typeText(windows.getKeyboardSequence().down(2));
    } else {
        spacingCombo.setValue("2,00");
    }

    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;

We finish the method by pressing Return:

1
2
   // Type Return to exit and apply the changes to the paragraph
    windows.typeText(windows.getKeyboardSequence().typeReturn());

The next method in the workflow copies the text and pastes it on Notepad, which remains opened.

We work with the objects HWND for both windows (Notepad and WordPad), to bring to the foreground the one we need. We will activate and bring to the foreground the window on which we want to paste the text that will be copied from WordPad.

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
public void copyAndPaste() throws Exception {
    
    // Start of the ninth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    
    // Obtain the HWND of Wordpad and Notepad
    HWND hWndWordpad =
            windows.getWindow("wordpad_example.*").gethWnd();
    HWND hWndNotepad =
            windows.getWindow("notepad_example.*").gethWnd();
    
    // Copy and paste the text
    String text = windows.copyAndGet();
    // Type the left key to see the change in the text
    windows.typeText(windows.getKeyboardSequence().left());
    windows.pause(PAUSE * 2);
    
    // If focus
    if (text == null || text.isEmpty()) {
        // Type text
        windows.typeText("Could not copy the text");
        server.setCurrentItemResultToWarn();
    } else {
        // Deactivate Wordpad
        windows.showWindow(hWndWordpad,
                EShowWindowState.SW_MINIMIZE);
        windows.pause(PAUSE);
        // Activate and maximize Notepad
        windows.showWindow(hWndNotepad,
                EShowWindowState.SW_SHOWMAXIMIZED);
        windows.pause(PAUSE);
        // Type the text
        windows.typeText(text);
        server.setCurrentItemResultToOK();
    }

    itemIndex++;
    currentItemIndex++;
}

Now we will close the opened files. We have decided not to save the changes, as mentioned earlier. We will close both windows, Notepad and WordPad, through Alt+F4.

Once again, we need the HWND for both windows. We have declared again two objects HWND for greater clarity, though they could have been declared outside the method in a real robot.

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
public void closeFiles() throws Exception {
    
    // Start of the tenth item processing
    server.setCurrentItem(currentItemIndex, items[itemIndex]);
    
    // Obtain the HWND of Wordpad and Notepad
    HWND hWndWordpad =
            windows.getWindow("wordpad_example.*").gethWnd();
    HWND hWndNotepad =
            windows.getWindow("notepad_example.*").gethWnd();
    
    // Use Alt+F4 to close the files
    if (hWndNotepad != null) {
        windows.showWindow(hWndNotepad,
                EShowWindowState.SW_SHOWMAXIMIZED);
        win-dows.typeText(windows.getKeyboardSequence().typeAltF(4));
        windows.pause(PAUSE);
        // If a dialog asks for saving the changes
        if (windows.getActiveWindowTitle().equals("Bloc de notas")) {
            /*
             * Choose no, pressing "No". Obtener HWND of the
             * confirmation * window.
             */
            HWND hWndConfirmationWindow =
                    windows.getForegroundWindow();
            if (!windows.clickButton(
                    windows.getControl(
                            hWndConfirmationWindow, 7))) {
                // Press button with Alt+N if failed
                windows.typeText(windows.getKeyboardSequence().
                        pressAlt().type("n").releaseAlt());
            }
        }
    }
    
    windows.pause(PAUSE);
    
    if (hWndWordpad != null) {
        windows.showWindow(hWndWordpad,
                EShowWindowState.SW_SHOWMAXIMIZED);
        win-dows.typeText(windows.getKeyboardSequence().typeAltF(4));
        windows.pause(PAUSE);
        // If a dialog asks if we want to save the changes
        if (windows.getActiveWindowTitle().equals("WordPad")) {
            // Choose no
            windows.typeText(windows.getKeyboardSequence().
                    pressAlt().type("n").releaseAlt());
        }
    }
    
    server.setCurrentItemResultToOK();
    itemIndex++;
    currentItemIndex++;
}

We reach the last method in the workflow: end. In this method, we show a warning message by using the method messageBox. We have already discussed this method, but it is worthy to be highlighted that when our robot shows messages and warnings to the user, it will wait until the human answers with one of the choices included in the message. After that, the robot will resume its work in one way or another depending on the option selected.

The method messageBox returns the option that has been chosen by the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void end() throws Exception {
    
    boolean freeHands = Boolean.valueOf(
server.getParameters().get("freeHands"));
    
    if (!freeHands) {
        // Show a message to notify the end of the execution
        windows.messageBox(windows.getForegroundWindow(),
"End of processing reached, the robot " +
"will wait for an answer to this message", "End",
                EMessageBox.MB_OK, EMessage-Box.MB_ICONINFORMATION);
    }
    
    windows.pause(PAUSE);
}

And we finish our class IRobot with the method cleanUp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @see com.novayre.jidoka.client.api.IRobot#cleanUp()
 */
@Override
public String[] cleanUp() throws Exception {
    
    // Kill processes wordpad.exe y notepad.exe
    if (windows.getProcess("wordpad.exe") != null) {
        windows.killAllProcesses("wordpad.exe", 1000);
    }
    if (windows.getProcess("notepad.exe") != null) {
        windows.killAllProcesses("notepad.exe", 1000);
    }
    
    return null;
}

Here, we ensure that we close all opened files. If our robot continues its execution without errors or warnings, then we can say these files have closed properly. But this method closes them otherwise. Remember that cleanUp will always run.

In the line:

1
windows.killAllProcesses("wordpad.exe", 1000) ;

we are using the method killAllProcesses(String name, long pauseBetweenProcess) which will close the processes specified by name, making a pause after each ending specified by the parameter pauseBetweenProcess.

Passing parameters to the robotic process from the workflow

It is possible to pass parameters to a robot method directly from the Workflow. For this, we must have defined the method in the robot following certain criteria that are explained below:

@JidokaMethod

With this annotation we will mark the methods that we want to pass parameters from the Workflow. These are the members that can be used in this annotation:

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
    /**
     * Paths on tree resource
     *
     * @return
     */
    String[] paths() default;
    
    /**
     * Name
     *
     * @return
     */
    String name();
    
    /**
     * Icon class
     *
     * @return
     */
    String iconClass() default "jf-console";
    
    /**
     * Description
     *
     * @return
     */
    String description();
    
    /**
     * Return class behavior
     *
     * @return
     */
    EJidokaMethodReturnClass returnClazz() default JidokaMethodReturnClass.AS_DEFINED;

A possible example of this annotation in a method of a robot would be:

1
2
@JidokaMethod(    paths =, name = "Wait any image", iconClass = "jf-console", 
                description = "Waits for any specified image using the default timeout.")

Once we have established that the method can receive parameters from the workflow it is time to define these parameters. This can be done through another annotation.

@JidokaParameter

Each of the parameters that we need to pass to the method needs to be written down in this way. The members of the annotation are:

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
    /**
     * Name
     * 
     * @return
     */
    String name();
    
    /**
     * Default value
     * 
     * @return
     */
    String defaultValue() default "";
    
    /**
     * JSON map
     * 
     * @return
     */
    String jsonMap() default "";
    
    /**
     * Type behavior
     * 
     * @return
     */
    EJidokaParameterType type() default EJidokaParameterType.AS_DEFINED;

The EJidokaParameterType member allows us to select what type of parameter we are establishing which has repercussions on how that parameter is seen from the workflow and how its values can be established. Let's see in detail the different options:

@EJidokaParameterType

AS_DEFINED

It is the default value so it will not be necessary to establish it. Parameters of this type will be used as defined. For example, Integer, Boolean, String, etc. Let's see an example:

1
2
@JidokaMethod(paths =, name = "Protect desktop", iconClass = "jf-console", description = "Protect (or not) desktop view.")
void protectDesktop(@JidokaParameter(name = "Protect?", defaultValue = "true") boolean protect);

And that's how it looks from the workflow when selecting the method:

2237376.png

ARRAY_OF_IMAGE_PATHS

It is used to select images that have been previously incorporated as support files.

1
@JidokaMethod(paths =, name = "Wait any image", iconClass = "jf-console", description = "Waits for any specified image using the default timeout.")

And when adding the images, we would see:

2237377.png

ENUMERATOR

Parameters of this type will only allow us to select a value from among the set that is defined in the corresponding enumerator:

1
2
@JidokaMethod(paths =, name = "Wait for Cursor Type", description = "Waits for the mouse cursor to be of a certain type")    
public void waitForCursorType(@JidokaParameter(name="Cursor Type", defaultValue="EClientCursorType.ARROW", type = EJidokaParameterType.ENUMERATOR) EClientCursorType mouseCursorType);  

TEXT_AREA

In this case, it is allowed to enter a String but the interface will offer a TextArea type text box to be able to conveniently enter larger texts:

1
2
3
4
5
@JidokaMethod(paths =, name = "Execute code", description = "Executes arbitrary code.")
void execCode(
        @JidokaParameter(name = "Language", defaultValue = "GROOVY", type = EJidokaParameterType.ENUMERATOR) EScriptLanguage language,
        @JidokaParameter(name = "Code", defaultValue = "", type = EJidokaParameterType.TEXT_AREA) String code
        );

And the result of this definition in the console:

2237380.png

JSON_MAP

Finally, we find the most flexible option of all, since it allows to enter a text in JSON format that can then be used facilemte from the code of the method itself:

1
2
@JidokaMethod(paths =, name = "Person trace", iconClass = "jf-console", description = "Traces person information like name and age.")
public void personTrace(@JidokaParameter(name = "Parameters", jsonMap = "{"fullName":{"name":"Full name", "defaultValue":""}, "age":{"name":"Age (years)", "defaultValue":"18"}}", type = EJidokaParameterType.JSON_MAP) SDKParameterMap parameters);

2237381.png

When using the parameters within the code, this would be the way to do it:

1
2
3
4
5
6
@JidokaMethod(paths =, name = "Person trace", iconClass = "jf-console", description = "Traces person information like name and age.")
public void personTrace(@JidokaParameter(name = "Parameters", jsonMap = "{"fullName":{"name":"Full name", "defaultValue":""}, "age":{"name":"Age (years)", "defaultValue":"18"}}", type = EJidokaParameterType.JSON_MAP) SDKParameterMap parameters) { 

    server.debug(String.format("Full name: %s", parameters.get("fullName"))); 
    server.debug(String.format("Age: %s", parameters.get("age")));
} 

Robot Java attribute as a parameter

From the workflow, if it calls a method of the robot, a method of a library or a direct API call, it is possible to specify Velocity code.

This way, properties defined in the code of the robot can be directly referenced from the workflow. To do so, Appian RPA provides access to some variables inside its context:

  • server: it's an instance of IJidokaServer and it must exist in our robot.
  • robot: it's the robot itself, an instance of IRobot.
  • client: it's the instance of IClient and it must exist in our robot.
  • parameters: it's a Map containing the instructions defined in the configuration of the robot.

This means that properties of each of these variables (for instance attributes of the robot) can be passed, provided that these properties must have getter and setter methods. Besides, it is possible to pass the result of a call to any method of these variables.

The expressions that can be used are the ones recognized by Velocity, for instance:

  • ${robot.attribute1}: It would access to the attribute1 attribute of the class of the robot.
  • ${robot.attribute2.subattribute3}: It would access to the subattribute3 attribute of the attribute2 attribute of the robot class.
  • ${robot.list[4]}: It would access to the 4th position of the list attribute of the robot class.
  • ${robot.map.get('my key 5')}: It would access, through the get method of Map, to the value corresponding to the 'my key 5' key of the map attribute of the robot class.
  • ${robot.map.get("my key 6")}: It would access, through the get method of Map, to the value corresponding to the "my key 6" key of the map attribute of the robot class. Notice the use of double quotes instead of single quotes.
  • ${robot.array[7]}: It would access to the 7th position of the array attribute of the robot class.
  • ${lastResult}: Result of the last action.

In the image below, we can see how the direct call to the API server->info is done passing as parameter the value in the 0th position of the negations attribute of the robot class.

2237394.png

Execution of scripts from the workflow

From the workflow, it is possible to call specifics methods of the API to execute arbitrary code. Currently, only Groovy scripts are supported.

To do so, you have to select, in the Modules tree, the option Robot -> Execute code or Robot -> Execute code with result. Once selected, we'll see that in the combo Language only the option GROOVY appears.

In the textarea Code, we must add the code to execute. Before giving an example of what code could be added, we have to bear in mind that Jidoka provides a context with several variables to be used, such us:

  • server: it's the IJidokaServer instance that must exist in our robot.
  • robot: it's the robot itself, an instance of IRobot.
  • client: it's the instance of IClient that must exist in our robot.

From these variables, we can invoke their attributes and methods. Additionally, it is possible to access to the robot instructions, which are available through the variable parameters, of type Map.

A very simple example of code is the one sending info-type log to the execution trace with the value of a robot attribute:

2237456.png

In this case, the log would be written with the value of the position 1 of the attribute negations of the robot class.

To make this code works, the server and robot instances must exist in our robot, as well as an attribute in the robot called negations of type array.

Use these tutorials to learn more about the Client module:

Open in Github Built: Fri, Nov 12, 2021 (02:39:09 PM)

On This Page

FEEDBACK