Prerequisites

Before proceeding with the tutorial, please install the following software:

  • Oracle VM VirtualBox
  • HashiCorp Vagrant
  • Avid Platform Virtual Machine

For instructions on setting up VirtualBox, Vagrant, and the Avid Platform Virtual Machine, see the README included in the Avid Platform Virtual Machine download file (avid-platform-vagrant-vm-YYYY-MM-DD-XXXX.zip).

In addition, make sure the gradle, java and javac commands are available in your shell. Ensure that the Java version is 7 or later and the Gradle version is 2.7 or later.

Note: The code for this tutorial is available in the examples/java-service directory.

Creating the Project Structure

  1. Create a directory for your project. We will use the name java-service in this tutorial. Then create a module subfolder inside.

    1
    2
    mkdir java-service
    mkdir java-service/module
  2. Make java-service/module directory current

    1
    cd java-service/module
  3. Create a build.gradle file

    1
    touch build.gradle
  4. Add the following content to this file:

    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
    apply plugin: 'java'

    apply from: "${projectDir}/versions.gradle"
    apply plugin: 'application'

    group = 'com.avid.connector.tutorial'
    version = '0.0.1-SNAPSHOT'
    applicationName = 'java-service'

    repositories {
    mavenCentral()

    flatDir {
    dirs '../../../libs'
    }
    }

    dependencies {
    compile fileTree(dir: '../../../libs', include: '*.jar')

    compile group: 'ch.qos.logback', name: 'logback-core', version: "${logbackVersion}"
    compile group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"

    compile group: 'com.google.code.findbugs', name: 'jsr305', version: "${jsr305}"
    }
  5. Create a versions.gradle file:

    1
    touch versions.gradle
  6. Add the following content to versions.gradle:

    1
    2
    3
    4
    ext {
    logbackVersion='1.1.3'
    jsr305 = '2.0.0'
    }
  7. Create src, src/main, src/test, src/main/java and src/test/java directories

    1
    2
    3
    4
    5
    mkdir src
    mkdir src/main
    mkdir src/test
    mkdir src/main/java
    mkdir src/test/java
  8. Verify the build by outputting its list of dependencies:

    1
    gradle dependencies

Congratulations! A simple project layout and core dependencies are now ready.

Connecting to the Platform

  1. Create a directory called src/main/java/com/avid/connector/tutorial:

    1
    mkdir -p src/main/java/com/avid/connector/tutorial
  2. Create a src/main/java/com/avid/connector/tutorial/JavaServiceMain.java file

    1
    touch src/main/java/com/avid/connector/tutorial/JavaServiceMain.java
  3. Add the following content to src/main/java/com/avid/connector/tutorial/JavaServiceMain.java

    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
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.connection.ConnectEvent;
    import com.avid.acs.bus.connection.ConnectionListener;
    import com.avid.acs.bus.connection.DisconnectEvent;
    import com.avid.acs.service.IpcBusAccessFactory;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    public class JavaServiceMain {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceMain.class);
    private static BusAccess bus;

    public static void main(String[] args) {
    try {
    bus = new IpcBusAccessFactory().createBusAccess(null, false);
    bus.connect(new ConnectionListener() {
    @Override
    public void onConnect(ConnectEvent event) {
    LOG.info("Connected to Avid Platform");
    }

    @Override
    public void onDisconnect(DisconnectEvent event) {
    LOG.info("Disconnected from Avid Platform");
    }
    });
    } catch (BusAccessException e) {
    LOG.error(e.getMessage(), e);
    }
    }
    }
  4. Modify the build.gradle file. Add the following line to the end of file:

    1
    mainClassName = 'com.avid.connector.tutorial.JavaServiceMain'
  5. Build the service:

    1
    gradle clean build installDist
  6. Set environment variables to use a websocket connection with a protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  7. Run the application you just created:

    1
    build/install/java-service/bin/java-service

    You should see following log output in the shell:

    1
    2
    3
    4
    5
    6
    7
    8
    11:06:23.371 [main] INFO  c.a.acs.service.IpcBusAccessFactory - IPC is 'Websocket with protocol buffers'
    11:06:23.631 [main] WARN com.avid.acs.utils.JarUtils - Missing 'ACS_PLATFORM_IDENTIFIER'. Using default 'unknown'.
    11:06:23.634 [main] WARN com.avid.acs.utils.JarUtils - Missing 'ACS_SERVICE_BUILD_NUMBER'. Using default 'unknown'.
    11:06:23.634 [main] WARN com.avid.acs.utils.JarUtils - Missing 'ACS_ENVIRONMENT_IDENTIFIER'. Using default 'unknown'.
    11:06:23.639 [main] INFO c.a.a.service.access.ProxyBusAccess - Connecting to the bus
    11:06:23.762 [main] DEBUG c.a.a.i.p.s.ServiceProtobufEndpoint - Starting wrapper client on host avid-platform-zone1, port 9900
    11:06:23.992 [main] INFO c.a.c.tutorial.JavaServiceMain - Connected to Avid Platform
    11:06:23.994 [main] INFO c.avid.acs.service.BusServiceControl - Gateway connection monitor is started
  8. Press Ctrl+C to terminate the service.

Congratulations! You have successfully connected to the Platform as a client.

Creating a Service

  1. Create src/main/java/com/avid/connector/tutorial/JavaService.java, src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java and src/main/java/com/avid/connector/tutorial/JavaServiceResponse.java files:

    1
    2
    3
    touch src/main/java/com/avid/connector/tutorial/JavaService.java
    touch src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java
    touch src/main/java/com/avid/connector/tutorial/JavaServiceResponse.java
  2. Add the following content to src/main/java/com/avid/connector/tutorial/JavaService.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.annotations.*;
    import com.avid.acs.bus.annotations.Error;
    import com.avid.acs.bus.OperationContext;
    import com.avid.acs.bus.error.ErrorSeverity;
    import com.avid.acs.bus.message.Message;
    import com.avid.acs.bus.service.info.RequestMethod;

    @BusService(type = "avid.tutorial.java.service", realm = "global", version = 1, desc = "Tutorial service based on Avid Connector API for Java")
    @Errors({
    @Error(code = "INVALID_JOB_ID", severity = ErrorSeverity.ERROR, messageTemplate = "Invalid jobId: %{jobId}", status = 500),
    @Error(code = "INVALID_EXECUTION_TIME", severity = ErrorSeverity.ERROR, messageTemplate = "Invalid execution time: %{execTime}", status = 500)
    })
    public interface JavaService {
    @Operation(name = "submitJob", desc = "Submit job for execution. Returns response back with information whether job submitted successfully or not")
    @Examples({
    @Example(name = "Submit valid job", file = "submit_valid_job.json"),
    @Example(name = "Submit invalid jobId", file = "submit_invalid_job_id.json"),
    @Example(name = "Submit invalid execTime", file = "submit_invalid_exec_time.json")
    })
    @RestRequest(path = "jobs/{jobId}", method = RequestMethod.POST, queryParams = { "execTime" })
    void submitJob(@Param("jobId") Integer jobId, @Param("execTime") Integer execTime, OperationContext<JavaServiceResponse> operationContext);
    }
  3. Add the following content to src/main/java/com/avid/connector/tutorial/JavaServiceResponse.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.avid.connector.tutorial;

    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;

    class JavaServiceResponse {
    @JsonProperty
    public final String jobStatus;

    @JsonCreator
    public JavaServiceResponse(@JsonProperty("jobStatus") String jobStatus) {
    this.jobStatus = jobStatus;
    }
    }
  4. Create a src/main/resources/com/avid/connector/tutorial directory:

    1
    mkdir -p src/main/resources/com/avid/connector/tutorial
  5. Add src/main/resources/com/avid/connector/tutorial/submit_valid_job.json, src/main/resources/com/avid/connector/tutorial/submit_invalid_job_id.json, src/main/resources/com/avid/connector/tutorial/submit_invalid_exec_time.json files:

    1
    2
    3
    touch src/main/resources/com/avid/connector/tutorial/submit_valid_job.json
    touch src/main/resources/com/avid/connector/tutorial/submit_invalid_job_id.json
    touch src/main/resources/com/avid/connector/tutorial/submit_invalid_exec_time.json
  6. Add the following content to src/main/resources/com/avid/connector/tutorial/submit_valid_job.json:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "serviceType": "avid.tutorial.java.service",
    "serviceRealm": "global",
    "serviceVersion": 3,
    "op": "submitJob",
    "paramSet": {
    "jobId": 1,
    "execTime": 1000
    }
    }
  7. Add the following content to src/main/resources/com/avid/connector/tutorial/submit_invalid_job_id.json:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "serviceType": "avid.tutorial.java.service",
    "serviceRealm": "global",
    "serviceVersion": 3,
    "op": "submitJob",
    "paramSet": {
    "jobId": -1,
    "execTime": 1000
    }
    }
  8. Add the following content to src/main/resources/com/avid/connector/tutorial/submit_invalid_exec_time.json:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "serviceType": "avid.tutorial.java.service",
    "serviceRealm": "global",
    "serviceVersion": 3,
    "op": "submitJob",
    "paramSet": {
    "jobId": 2,
    "execTime": 10
    }
    }
  9. Add the following content to src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.error.BusError;

    import java.util.*;

    public class JavaServiceImpl implements JavaService {
    @Override
    public void submitJob(Integer jobId, Integer execTime, OperationContext<JavaServiceResponse> operationContext) {
    if (jobId == null || jobId < 1) {
    Map<String, String> params = Collections.singletonMap("jobId", jobId == null ? "null" : String.valueOf(jobId));
    operationContext.error(new BusError("INVALID_JOB_ID", params, "Invalid jobId was provided. Must be integer > 0."));
    } else if (execTime == null || execTime < 100) {
    Map<String, String> params = Collections.singletonMap("execTime", execTime == null ? "null" : String.valueOf(execTime));
    operationContext.error(new BusError("INVALID_EXECUTION_TIME", params, "Invalid execution time provided. Must be integer >= 100."));
    } else {
    JavaServiceResponse response = new JavaServiceResponse("submitted");
    operationContext.respond(response);
    }
    }
    }
  10. Modify the src/main/java/com/avid/connector/tutorial/JavaServiceMain.java file:

    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
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.connection.ConnectEvent;
    import com.avid.acs.bus.connection.ConnectionListener;
    import com.avid.acs.bus.connection.DisconnectEvent;
    import com.avid.acs.service.IpcBusAccessFactory;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    public class JavaServiceMain {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceMain.class);
    private static BusAccess bus;

    public static void main(String[] args) {
    try {
    bus = new IpcBusAccessFactory().createBusAccess(null, false);
    bus.connect(new ConnectionListener() {
    @Override
    public void onConnect(ConnectEvent event) {
    LOG.info("Connected to Avid Platform");
    }

    @Override
    public void onDisconnect(DisconnectEvent event) {
    LOG.info("Disconnected from Avid Platform");
    }
    });

    Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
    bus.disconnect();
    }
    });

    JavaService javaService = new JavaServiceImpl();
    AsyncCallback<ServiceContext> asyncCallback = new AsyncCallback<ServiceContext>() {
    @Override
    public void onSuccess(ServiceContext serviceContext) {
    LOG.info("Service registered with id={}", serviceContext.getServiceInfo().getId());
    }

    @Override
    public void onError(CallbackError callbackError) {
    LOG.info("Failed to register service: {}", callbackError.getMessage());
    }
    };
    bus.registerService(javaService, null, asyncCallback);
    } catch (BusAccessException e) {
    LOG.error(e.getMessage(), e);
    }
    }
    }
  11. Build the service:

    1
    gradle clean build installDist
  12. Set environment variables to use a websocket connection with a protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  13. Run the application:

    1
    build/install/java-service/bin/java-service

    You should see logs output in the shell.

  14. Test that the service is working:

    • Open the ACS Monitor. Filter the services list by entering java\.service in the Service Types Filter field and clicking the Apply button. The list is reduced to avid.tutorial.java.service.
    • Expand the Service Operations List for the service. The list should include the submitJob operation.
    • Click on the Submit valid job link. Its corresponding message should appear in the code editor area, under the Request to field.
    • Click the Query button. Your service’s response appears in the Response field. Your service’s response appears in the Response field.
  15. Press Ctrl+C in the shell to terminate the service.

Congratulations! You have successfully registered your service on the Platform.

Sending Requests to Another Service

  1. Modify src/main/java/com/avid/connector/tutorial/JavaServiceResponse.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.avid.connector.tutorial;

    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;

    class JavaServiceResponse {
    @JsonProperty
    public final String jobStatus;
    @JsonProperty
    public final Integer numberOfSubmittedJobs;

    @JsonCreator
    public JavaServiceResponse(@JsonProperty("jobStatus") String jobStatus, @JsonProperty("numberOfSubmittedJobs") Integer numberOfSubmittedJobs) {
    this.jobStatus = jobStatus;
    this.numberOfSubmittedJobs = numberOfSubmittedJobs;
    }
    }
  2. In this step you modify the submitJob method in the src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java file. You add a constructor and two new methods, setNumberOfSubmittedJobs and getNumberOfSubmittedJobs, to update and request the number of submitted jobs. In addition, you add methods and interfaces to handle asynchronous requests:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.BusAccess;
    import com.avid.acs.bus.BusAccessException;
    import com.avid.acs.bus.OperationContext;
    import com.avid.acs.bus.ResponseHandler;
    import com.avid.acs.bus.error.BusError;
    import com.avid.acs.bus.error.ErrorSet;
    import com.avid.acs.bus.message.Message;
    import com.avid.acs.bus.message.MessageOptions;
    import com.avid.acs.bus.message.MutableMessage;
    import com.avid.acs.bus.message.data.Data;
    import com.avid.acs.bus.message.data.JsonData;
    import com.fasterxml.jackson.databind.JsonNode;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import javax.annotation.Nonnull;
    import java.util.Collections;
    import java.util.Map;

    public class JavaServiceImpl implements JavaService {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceImpl.class);

    private final BusAccess busAccess;

    public JavaServiceImpl(BusAccess busAccess) {
    this.busAccess = busAccess;
    }

    @Override
    public void submitJob(Integer jobId, Integer execTime, final OperationContext<JavaServiceResponse> operationContext) {
    if (jobId == null || jobId < 1) {
    Map<String, String> params = Collections.singletonMap("jobId", jobId == null ? "null" : String.valueOf(jobId));
    operationContext.error(new BusError("INVALID_JOB_ID", params, "Invalid jobId was provided. Must be integer > 0."));
    } else if (execTime == null || execTime < 100) {
    Map<String, String> params = Collections.singletonMap("execTime", execTime == null ? "null" : String.valueOf(execTime));
    operationContext.error(new BusError("INVALID_EXECUTION_TIME", params, "Invalid execution time provided. Must be integer >= 100."));
    } else {
    final SetNumberOfSubmittedJobsCallback setNumberOfSubmittedJobsCallback = new SetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber) {
    JavaServiceResponse response;
    if (responseMissing || errors.hasErrors()) {
    response = new JavaServiceResponse("submitted", oldNumber);
    } else {
    response = new JavaServiceResponse("submitted", newNumber);
    }

    operationContext.respond(response);
    }
    };

    final GetNumberOfSubmittedJobsCallback getNumberOfSubmittedJobsCallback = new GetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer number) {
    Integer newNumber;

    if (!responseMissing && !errors.hasErrors()) {
    newNumber = number + 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else if (errors != null && errors.hasErrors() && "avid.acs.attributes/NOT_FOUND".equals(errors.iterator().next().getCode())) {
    newNumber = 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else {
    JavaServiceResponse response = new JavaServiceResponse("submitted", null);
    operationContext.respond(response);
    }
    }
    };


    getNumberOfSubmittedJobs(getNumberOfSubmittedJobsCallback);
    }
    }

    private void getNumberOfSubmittedJobs(GetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "fetch");
    message.parameters().put("name", "java.service.store");

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, createGetNumberOfSubmittedJobsResponseHandler(callback), options);
    } catch (BusAccessException e) {
    callback.handle(true, null, null);
    }
    }

    private void setNumberOfSubmittedJobs(Integer number, int newNumber, SetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "store");
    message.parameters().put("name", "java.service.store");
    message.parameters().put("value", Collections.singletonMap("numberOfSubmittedJobs", newNumber));

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, createSetNumberOfSubmittedJobsResponseHandler(callback, number, newNumber), options);
    } catch (BusAccessException e) {
    callback.handle(true, null, number, newNumber);
    }
    }

    private ResponseHandler createGetNumberOfSubmittedJobsResponseHandler(final GetNumberOfSubmittedJobsCallback callback) {
    return new ResponseHandler() {
    @Override
    public void onResponse(@Nonnull Message response) {
    final Data value = response.results().get("value");
    Integer number = null;

    if (value != null && value instanceof JsonData) {
    final JsonNode jsonNode = ((JsonData) value).get().get("numberOfSubmittedJobs");
    if (jsonNode != null) {
    number = jsonNode.asInt();
    }
    } else {
    if (value == null) {
    number = 0;
    }
    }

    callback.handle(false, response.errors(), number);
    }

    @Override
    public void onMessageProcessingError(@Nonnull String error) {
    LOG.info("Error occurred");
    callback.handle(true, null, null);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, null);
    }
    };
    }

    private ResponseHandler createSetNumberOfSubmittedJobsResponseHandler(final SetNumberOfSubmittedJobsCallback callback, final Integer number, final Integer newNumber) {
    return new ResponseHandler() {

    @Override
    public void onResponse(@Nonnull Message response) {
    callback.handle(false, response.errors(), number, newNumber);
    }

    @Override
    public void onMessageProcessingError(@Nonnull String error) {
    LOG.info("Error occurred");
    callback.handle(true, null, number, newNumber);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, number, newNumber);
    }
    };
    }

    private interface GetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer number);
    }

    private interface SetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber);
    }
    }
  3. Next, modify src/main/java/com/avid/connector/tutorial/JavaServiceMain.java, adding a bus instance to the constructor of JavaServiceImpl:

    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
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.connection.ConnectEvent;
    import com.avid.acs.bus.connection.ConnectionListener;
    import com.avid.acs.bus.connection.DisconnectEvent;
    import com.avid.acs.service.IpcBusAccessFactory;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    public class JavaServiceMain {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceMain.class);
    private static BusAccess bus;

    public static void main(String[] args) {
    try {
    bus = new IpcBusAccessFactory().createBusAccess(null, false);
    bus.connect(new ConnectionListener() {
    @Override
    public void onConnect(ConnectEvent event) {
    LOG.info("Connected to Avid Platform");
    }

    @Override
    public void onDisconnect(DisconnectEvent event) {
    LOG.info("Disconnected from Avid Platform");
    }
    });
    JavaService javaService = new JavaServiceImpl(bus);
    AsyncCallback<ServiceContext> asyncCallback = new AsyncCallback<ServiceContext>() {
    @Override
    public void onSuccess(ServiceContext serviceContext) {
    LOG.info("Service registered with id={}", serviceContext.getServiceInfo().getId());
    }

    @Override
    public void onError(CallbackError callbackError) {
    LOG.info("Failed to register service: {}", callbackError.getMessage());
    }
    };
    bus.registerService(javaService, null, asyncCallback);
    } catch (BusAccessException e) {
    LOG.error(e.getMessage(), e);
    }
    }
    }
  4. Build the service:

    1
    gradle clean build installDist
  5. Set environment variables to use a websocket connection with protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  6. Run the application:

    1
    build/install/java-service/bin/java-service

    You should see logs output in the shell.

  7. Test that the service is working. Open ACS Monitor, and send several submitJob requests to your service. Each time you send a new request, you should see the number of submitted jobs increment in the response:

    1
    2
    3
    4
    5
    6
    {
    "resultSet": {
    "jobStatus": "submitted",
    "numberOfSubmittedJobs": 2
    }
    }
  8. Press Ctrl+C in the shell to terminate the service.

Congratulations! You have successfully sent a request to another service, and received a response from it.

Posting Events when the Job is Submitted And Completed

  1. Edit the src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java file. Here, we modify the submitJob method and add new postChannelMessages method:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.channel.ChannelMessage;
    import com.avid.acs.bus.channel.MutableChannelMessage;
    import com.avid.acs.bus.error.BusError;
    import com.avid.acs.bus.error.ErrorSet;
    import com.avid.acs.bus.message.Message;
    import com.avid.acs.bus.message.MessageOptions;
    import com.avid.acs.bus.message.MutableMessage;
    import com.avid.acs.bus.message.data.Data;
    import com.avid.acs.bus.message.data.JsonData;
    import com.fasterxml.jackson.databind.JsonNode;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import java.util.Collections;
    import java.util.Map;
    import java.util.Timer;
    import java.util.TimerTask;

    public class JavaServiceImpl implements JavaService {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceImpl.class);

    private final BusAccess busAccess;

    public JavaServiceImpl(BusAccess busAccess) {
    this.busAccess = busAccess;
    }

    @Override
    public void submitJob(final Integer jobId, Integer execTime, final OperationContext<JavaServiceResponse> operationContext) {
    if (jobId == null || jobId < 1) {
    Map<String, String> params = Collections.singletonMap("jobId", jobId == null ? "null" : String.valueOf(jobId));
    operationContext.error(new BusError("INVALID_JOB_ID", params, "Invalid jobId was provided. Must be integer > 0."));
    } else if (execTime == null || execTime < 100) {
    Map<String, String> params = Collections.singletonMap("execTime", execTime == null ? "null" : String.valueOf(execTime));
    operationContext.error(new BusError("INVALID_EXECUTION_TIME", params, "Invalid execution time provided. Must be integer >= 100."));
    } else {
    postChannelMessages(jobId, execTime);

    final SetNumberOfSubmittedJobsCallback setNumberOfSubmittedJobsCallback = new SetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber) {
    JavaServiceResponse response;
    if (responseMissing || errors.hasErrors()) {
    response = new JavaServiceResponse("submitted", oldNumber);
    } else {
    response = new JavaServiceResponse("submitted", newNumber);
    }

    operationContext.respond(response);
    }
    };

    final GetNumberOfSubmittedJobsCallback getNumberOfSubmittedJobsCallback = new GetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer number) {
    Integer newNumber;

    if (!responseMissing && !errors.hasErrors()) {
    newNumber = number + 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else if (errors != null && errors.hasErrors() && "avid.acs.attributes/NOT_FOUND".equals(errors.iterator().next().getCode())) {
    newNumber = 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else {
    JavaServiceResponse response = new JavaServiceResponse("submitted", null);
    operationContext.respond(response);
    }
    }
    };


    getNumberOfSubmittedJobs(getNumberOfSubmittedJobsCallback);
    }
    }

    private void postChannelMessages(final Integer jobId, Integer execTime) {
    ChannelMessage message = new MutableChannelMessage("java.service.jobs", "submit");
    message.data().put("jobId", jobId);

    try {
    busAccess.postToChannel(message, getPostToChannelAsyncCallback());
    } catch (BusAccessException e) {
    LOG.error("Failed to post channel message", e);
    }

    new Timer().schedule(new TimerTask() {
    @Override
    public void run() {
    ChannelMessage message = new MutableChannelMessage("java.service.jobs", "complete");
    message.data().put("jobId", jobId);

    try {
    busAccess.postToChannel(message, getPostToChannelAsyncCallback());
    } catch (BusAccessException e) {
    LOG.error("Failed to post channel message", e);
    }
    }
    }, execTime);
    }

    private AsyncCallback<Void> getPostToChannelAsyncCallback() {
    return new AsyncCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
    LOG.info("Successfully posted to channel");
    }

    @Override
    public void onError(CallbackError callbackError) {
    LOG.info("Failed post to channel: {}", callbackError.getMessage());
    }
    };
    }

    private void getNumberOfSubmittedJobs(GetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "fetch");
    message.parameters().put("name", "java.service.store");

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, options, createGetNumberOfSubmittedJobsCallback(callback));
    } catch (BusAccessException e) {
    callback.handle(true, null, null);
    }
    }

    private void setNumberOfSubmittedJobs(Integer number, int newNumber, SetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "store");
    message.parameters().put("name", "java.service.store");
    message.parameters().put("value", Collections.singletonMap("numberOfSubmittedJobs", newNumber));

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, options, createSetNumberOfSubmittedJobsCallback(callback, number, newNumber));
    } catch (BusAccessException e) {
    callback.handle(true, null, number, newNumber);
    }
    }

    private QueryAsyncCallback createGetNumberOfSubmittedJobsCallback(final GetNumberOfSubmittedJobsCallback callback) {
    return new QueryAsyncCallback() {
    @Override
    public void onSuccess(Message response) {
    final Data value = response.results().get("value");
    Integer number = null;

    if (value != null && value instanceof JsonData) {
    final JsonNode jsonNode = ((JsonData) value).get().get("numberOfSubmittedJobs");
    if (jsonNode != null) {
    number = jsonNode.asInt();
    }
    }
    callback.handle(false, response.errors(), number);
    }

    @Override
    public void onError(CallbackError error) {
    LOG.info("Error occurred");
    callback.handle(true, null, null);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, null);
    }
    };
    }

    private QueryAsyncCallback createSetNumberOfSubmittedJobsCallback(final SetNumberOfSubmittedJobsCallback callback, final Integer number, final Integer newNumber) {
    return new QueryAsyncCallback() {

    @Override
    public void onSuccess(Message response) {
    callback.handle(false, response.errors(), number, newNumber);
    }

    @Override
    public void onError(CallbackError error) {
    LOG.info("Error occurred");
    callback.handle(true, null, number, newNumber);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, number, newNumber);
    }
    };
    }

    private interface GetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer number);
    }

    private interface SetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber);
    }
    }
  2. Build the service:

    1
    gradle clean build installDist
  3. Set environment variables to use a websocket connection with a protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  4. Run the application:

    1
    build/install/java-service/bin/java-service

    You should see logs output in the shell.

  5. Press Ctrl+C in the shell to terminate the service.

Congratulations! You have successfully published events to the Platform.

At this point the service behavior in the ACS Monitor is unchanged. Next, we create a consumer of our service events.

Subscribing to Service Events

  1. Create a src/main/java/com/avid/connector/tutorial/JavaClientMain.java file:

    1
    touch src/main/java/com/avid/connector/tutorial/JavaClientMain.java
  2. Add the following content to the file:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.AsyncCallback;
    import com.avid.acs.bus.BusAccess;
    import com.avid.acs.bus.BusAccessException;
    import com.avid.acs.bus.CallbackError;
    import com.avid.acs.bus.channel.ChannelMessage;
    import com.avid.acs.bus.channel.ChannelSubscriber;
    import com.avid.acs.bus.connection.ConnectEvent;
    import com.avid.acs.bus.connection.ConnectionListener;
    import com.avid.acs.bus.connection.DisconnectEvent;
    import com.avid.acs.service.IpcBusAccessFactory;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import javax.annotation.Nonnull;
    import java.util.Collections;

    public class JavaClientMain {
    private static final Logger LOG = LoggerFactory.getLogger(JavaClientMain.class);
    private static BusAccess bus;

    public static void main(String[] args) {
    try {
    bus = new IpcBusAccessFactory().createBusAccess(null, false);
    bus.connect(new ConnectionListener() {
    @Override
    public void onConnect(ConnectEvent event) {
    LOG.info("Connected to Avid Platform");
    }

    @Override
    public void onDisconnect(DisconnectEvent event) {
    LOG.info("Disconnected from Avid Platform");
    }
    });

    AsyncCallback<Void> asyncCallback = new AsyncCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
    LOG.info("Successfully subscribed to channel");
    }

    @Override
    public void onError(CallbackError callbackError) {
    LOG.info("Subscribe to channel failed: {}", callbackError.getMessage());
    }
    };
    bus.subscribeToChannel("java.service.jobs", Collections.singletonList("submit"), createSubmitSubscriber(), null, asyncCallback);
    bus.subscribeToChannel("java.service.jobs", Collections.singletonList("complete"), createCompleteSubscriber(), null, asyncCallback);
    } catch (BusAccessException e) {
    LOG.error(e.getMessage(), e);
    }
    }

    private static ChannelSubscriber createSubmitSubscriber() {
    return new ChannelSubscriber() {
    @Override
    public void onChannelMessage(@Nonnull ChannelMessage channelMessage) {
    LOG.info("Job {} is submitted", channelMessage.data().get("jobId").asInt());
    }

    @Override
    public void onEndOfChannel(@Nonnull String name) {
    LOG.info("Deprecated, not used method");
    }
    };
    }

    private static ChannelSubscriber createCompleteSubscriber() {
    return new ChannelSubscriber() {
    @Override
    public void onChannelMessage(@Nonnull ChannelMessage channelMessage) {
    LOG.info("Job {} is completed", channelMessage.data().get("jobId").asInt());
    }

    @Override
    public void onEndOfChannel(@Nonnull String name) {
    LOG.info("Deprecated, not used method");
    }
    };
    }
    }
  3. Modify the build.gradle file to support multiple main classes for the application plugin:

    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
    apply plugin: 'java'

    apply from: "${projectDir}/versions.gradle"
    apply plugin: 'application'

    group = 'com.avid.connector.tutorial'
    version = '0.0.1-SNAPSHOT'
    applicationName = 'java-service'

    repositories {
    mavenCentral()

    flatDir {
    dirs '../../../libs'
    }
    }

    dependencies {
    compile fileTree(dir: '../../../libs', include: '*.jar')

    compile group: 'ch.qos.logback', name: 'logback-core', version: "${logbackVersion}"
    compile group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"

    compile group: 'com.google.code.findbugs', name: 'jsr305', version: "${jsr305}"
    }

    mainClassName = 'com.avid.connector.tutorial.JavaServiceMain'

    task moreStartScripts(type: CreateStartScripts) {
    mainClassName = "com.avid.connector.tutorial.JavaClientMain"
    applicationName = "java-client"
    outputDir = new File(project.buildDir, 'scripts')
    classpath = jar.outputs.files + project.configurations.runtime
    }

    applicationDistribution.into("bin") {
    from(moreStartScripts)
    fileMode = 0755
    }
  4. Build the service:

    1
    gradle clean build installDist
  5. Set environment variables to use a websocket connection with a protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  6. Run the service:

    1
    build/install/java-service/bin/java-service
  7. Open a new shell in the same location. Set environment variables to use websocket connection with protobuf payload:

    1
    export ACS_GATEWAY_HOST=avid-platform-zone1
  8. Run the client:

    1
    build/install/java-service/bin/java-client
  9. Test that the service and client are working:

    • Open ACS Monitor, and filter the services list by entering the java\.service regular expression in the box. You should now see only one service in the list: avid.tutorial.java.service.
    • Send multiple submitJob messages to your service with different values. Examine the logs of your client. The logs ought to contain each “job submit” and “job complete” event.

Congratulations! You have created a client that subscribes to the events being posted by your service.

Sending REST Requests to Your Service

  1. Log in to avid-platform-zone1 with ssh (username: root; password: vagrant):

    1
    ssh root@avid-platform-zone1
  2. Send a POST request to your service using the curl command. Use the avidAccessToken generated previously:

    1
    2
    3
    curl -H "Content-Type: application/json" --data '{"execTime":15000}' "http://avid-platform-zone1:8080/apis/avid.tutorial.java.service;version=1/jobs/10?_avidAccessToken=ZWEwZDhlOTItYmJhZS00YTQ5LWFiN2QtNTAyYWMwZjhjZjJj"

    #NOTE: The request should be routed via NGINX and use the HTTPS protocol: https://avid-platform-zone1/apis/...

    Given HTTP request will send following message to your service:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "serviceType": "avid.tutorial.java.service",
    "serviceRealm": "global",
    "serviceVersion": 1,
    "op": "submitJob",
    "paramSet": {
    "jobId": 10,
    "execTime": 15000
    }
    }
  3. Press Ctrl+C in the shells to terminate both the service and client.

Delivering the Service as an RPM on CentOS 6.x/RedHat 6.x

For this tutorial and the next one, you need a CentOS 6.x/RedHat 6.x system. You may use your VM (created in the first steps of this tutorial) with the host name avid-platform-zone1. The following steps assume you are using this VM to test the RPM.

  1. Add a Gradle wrapper to your build. The wrapper allows the service to operate, even if Gradle is not in the PATH:

    1
    gradle wrapper
  2. At this point your current directory should be java-service/module. Go up one level. Your current directory must be java-service:

    1
    cd ..
  3. Create a java-service.spec file:

    1
    touch java-service.spec
  4. Add the following content to the file:

    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
    %global debug_package %{nil}

    Name: java-service
    Version: 1
    Release: 1
    Summary: Tutorial service based on Avid Connector API for Java
    License: Avid EULA
    Vendor: Avid Technology Inc.
    URL: http://avid.com
    Source: %{name}-src.tgz
    BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
    Prefix: /opt/avid

    AutoReqProv: no

    Requires: jre-1.7.0-avid-openjdk-x86_64

    %description
    Tutorial service based on Avid Connector API for Java

    %build
    if [ ! -f module/build/install/%{name}/bin/%{name} ]
    then
    echo "You have to call 'module/gradlew -p module clean installDist' first"
    exit -1
    fi

    %prep
    %setup -c -n %{name}

    %install
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/sbin"
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/bin"
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/lib"

    mv module/build/install/java-service/bin/* "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/bin/"
    mv module/build/install/java-service/lib/* "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/lib/"
    ln -s /opt/avid/lib/java_modules/%{name}/bin/%{name} "${RPM_BUILD_ROOT}/opt/avid/sbin/%{name}"

    # Exit clean
    exit 0

    %files
    %attr(755, root, root) /opt/avid/lib/java_modules/%{name}/bin/%{name}
    /opt/avid/sbin/%{name}
    /opt/avid/lib/java_modules/%{name}
  5. Create a create-rpm.sh file and make it executable:

    1
    2
       touch create-rpm.sh
    chmod 755 create-rpm.sh
  6. Add the following content to the file:

    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
    #!/bin/bash

    # Print usage if we have no args
    if [ $# -eq 0 ]; then
    echo 'Usage:'
    echo ' create-rpm <spec-file>'
    echo 'Description:'
    echo ' Used by jenkins jobs to create rpms. Specify'
    echo ' the spec file and this script will tar the'
    echo ' directory containing that file and run rpmbuild'
    echo ' in the parent dir. See source for details.'
    echo 'Examples:'
    echo ' ./create-rpm.sh controllers/registry/avid-acs-registry.spec'
    echo ' Creates ./rpmbuild/RPMS/x86_64/<rpm-name>.rpm'
    exit 0
    fi

    RPM_VERSION=${RPM_VERSION-"0.0.0_DEV"}
    if [ -f ./VERSION ]; then
    # file generated by jenkins job
    RPM_VERSION="$(cat ./VERSION)"
    fi

    ORIGINAL_DIR="$(pwd)"

    # The directory that we are going to tar
    SOURCE_DIR="$(dirname $1)"

    # The base name of the spec file. This name will be used
    # for the tar file.
    SPEC_NAME="$(basename $1 .spec)"

    # Set up rpmbuild tree
    mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

    pushd "$SOURCE_DIR"

    # Compress sources and move into rpmbuild tree
    tar -czf "$SPEC_NAME-src.tgz" ./* --exclude=.acignore --exclude=node_modules --exclude=*.swp --exclude=*~
    mv "$SPEC_NAME-src.tgz" "$ORIGINAL_DIR/rpmbuild/SOURCES/"

    # Copy spec into rpmbuild tree
    cp "$SPEC_NAME.spec" "$ORIGINAL_DIR/rpmbuild/SPECS/"

    popd

    # Build rpm
    rpmbuild --define "_rpmversion $RPM_VERSION" --define "_topdir `pwd`/rpmbuild" -bb "rpmbuild/SPECS/$SPEC_NAME.spec"
  7. Build your project:

    1
    module/gradlew -p module clean installDist
  8. Copy your project to avid-platform-zone1 using ssh (username: root; password: vagrant). Your current directory should be java-service:

    1
    scp -r ../java-service root@avid-platform-zone1:/root/
  9. Log in to avid-platform-zone1 using ssh (username: root; password: vagrant):

    1
    ssh root@avid-platform-zone1
  10. In the ssh shell change your current directory:

    1
    cd /root/java-service
  11. Install the needed RPM development tools:

    1
    yum install rpm-devel
  12. Make a create-rpm.sh executable and create the RPM:

    1
    2
    chmod 755 create-rpm.sh
    ./create-rpm.sh java-service.spec
  13. Install your RPM:

    1
    yum install rpmbuild/RPMS/x86_64/java-service-1-1.x86_64.rpm
  14. Run the installed service:

    1
    /opt/avid/sbin/java-service
  15. Test that the service is working in the ACS Monitor

  16. Press Ctrl+C to terminate the service.

Congratulations! You have successfully created an RPM for your service, installed and started it.

Daemonizing the Service on CentOS 6.x/RedHat 6.x

In this section we daemonize the service, provide a default configuration for it, output logs into a file and add log rotation. If you are still in the ssh shell from the previous section, exit and return to your project directory. Your current directory must be java-service.

  1. Modify the module/src/main/java/com/avid/connector/tutorial/JavaServiceImpl.java file. Add the serviceIsRegistered and reportServiceIsReady methods:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    package com.avid.connector.tutorial;

    import com.avid.acs.bus.*;
    import com.avid.acs.bus.annotations.Subscribe;
    import com.avid.acs.bus.channel.ChannelMessage;
    import com.avid.acs.bus.channel.MutableChannelMessage;
    import com.avid.acs.bus.error.BusError;
    import com.avid.acs.bus.error.ErrorSet;
    import com.avid.acs.bus.message.Message;
    import com.avid.acs.bus.message.MessageOptions;
    import com.avid.acs.bus.message.MutableMessage;
    import com.avid.acs.bus.message.data.Data;
    import com.avid.acs.bus.message.data.JsonData;
    import com.avid.acs.bus.service.events.ServiceRegisteredEvent;
    import com.fasterxml.jackson.databind.JsonNode;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import java.io.File;
    import java.io.IOException;
    import java.util.Collections;
    import java.util.Map;
    import java.util.Timer;
    import java.util.TimerTask;

    public class JavaServiceImpl implements JavaService {
    private static final Logger LOG = LoggerFactory.getLogger(JavaServiceImpl.class);

    private final BusAccess busAccess;

    public JavaServiceImpl(BusAccess busAccess) {
    this.busAccess = busAccess;
    }

    @Subscribe
    public void serviceIsRegistered(ServiceRegisteredEvent event) {
    reportServiceIsReady();
    }

    @Override
    public void submitJob(final Integer jobId, Integer execTime, final OperationContext<JavaServiceResponse> operationContext) {
    if (jobId == null || jobId < 1) {
    Map<String, String> params = Collections.singletonMap("jobId", jobId == null ? "null" : String.valueOf(jobId));
    operationContext.error(new BusError("INVALID_JOB_ID", params, "Invalid jobId was provided. Must be integer > 0."));
    } else if (execTime == null || execTime < 100) {
    Map<String, String> params = Collections.singletonMap("execTime", execTime == null ? "null" : String.valueOf(execTime));
    operationContext.error(new BusError("INVALID_EXECUTION_TIME", params, "Invalid execution time provided. Must be integer >= 100."));
    } else {
    postChannelMessages(jobId, execTime);

    final SetNumberOfSubmittedJobsCallback setNumberOfSubmittedJobsCallback = new SetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber) {
    JavaServiceResponse response;
    if (responseMissing || errors.hasErrors()) {
    response = new JavaServiceResponse("submitted", oldNumber);
    } else {
    response = new JavaServiceResponse("submitted", newNumber);
    }

    operationContext.respond(response);
    }
    };

    final GetNumberOfSubmittedJobsCallback getNumberOfSubmittedJobsCallback = new GetNumberOfSubmittedJobsCallback() {
    @Override
    public void handle(boolean responseMissing, ErrorSet errors, Integer number) {
    Integer newNumber;

    if (!responseMissing && !errors.hasErrors()) {
    newNumber = number + 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else if (errors != null && errors.hasErrors() && "avid.acs.attributes/NOT_FOUND".equals(errors.iterator().next().getCode())) {
    newNumber = 1;
    setNumberOfSubmittedJobs(number, newNumber, setNumberOfSubmittedJobsCallback);
    } else {
    JavaServiceResponse response = new JavaServiceResponse("submitted", null);
    operationContext.respond(response);
    }
    }
    };


    getNumberOfSubmittedJobs(getNumberOfSubmittedJobsCallback);
    }
    }

    private void postChannelMessages(final Integer jobId, Integer execTime) {
    ChannelMessage message = new MutableChannelMessage("java.service.jobs", "submit");
    message.data().put("jobId", jobId);

    try {
    busAccess.postToChannel(message, getPostToChannelAsyncCallback());
    } catch (BusAccessException e) {
    LOG.error("Failed to post channel message", e);
    }

    final Timer timer = new Timer();
    timer.schedule(new TimerTask() {
    @Override
    public void run() {
    ChannelMessage message = new MutableChannelMessage("java.service.jobs", "complete");
    message.data().put("jobId", jobId);

    try {
    busAccess.postToChannel(message, getPostToChannelAsyncCallback());
    } catch (BusAccessException e) {
    LOG.error("Failed to post channel message", e);
    }

    timer.cancel();
    }
    }, execTime);
    }

    private AsyncCallback<Void> getPostToChannelAsyncCallback() {
    return new AsyncCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
    LOG.info("Successfully posted to channel");
    }

    @Override
    public void onError(CallbackError callbackError) {
    LOG.info("Failed post to channel: {}", callbackError.getMessage());
    }
    };
    }

    private void getNumberOfSubmittedJobs(GetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "fetch");
    message.parameters().put("name", "java.service.store");

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, createGetNumberOfSubmittedJobsResponseHandler(callback), options);
    } catch (BusAccessException e) {
    callback.handle(true, null, null);
    }
    }

    private void setNumberOfSubmittedJobs(Integer number, int newNumber, SetNumberOfSubmittedJobsCallback callback) {
    Message message = new MutableMessage("avid.acs.attributes", "global", 3, "store");
    message.parameters().put("name", "java.service.store");
    message.parameters().put("value", Collections.singletonMap("numberOfSubmittedJobs", newNumber));

    try {
    MessageOptions options = new MessageOptions(2000);
    busAccess.query(message, createSetNumberOfSubmittedJobsResponseHandler(callback, number, newNumber), options);
    } catch (BusAccessException e) {
    callback.handle(true, null, number, newNumber);
    }
    }

    private ResponseHandler createGetNumberOfSubmittedJobsResponseHandler(final GetNumberOfSubmittedJobsCallback callback) {
    return new ResponseHandler() {
    @Override
    public void onResponse(@Nonnull Message response) {
    final Data value = response.results().get("value");
    Integer number = null;

    if (value != null && value instanceof JsonData) {
    final JsonNode jsonNode = ((JsonData) value).get().get("numberOfSubmittedJobs");
    if (jsonNode != null) {
    number = jsonNode.asInt();
    }
    }
    callback.handle(false, response.errors(), number);
    }

    @Override
    public void onMessageProcessingError(@Nonnull String error) {
    LOG.info("Error occurred");
    callback.handle(true, null, null);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, null);
    }
    };
    }

    private ResponseHandler createSetNumberOfSubmittedJobsResponseHandler(final SetNumberOfSubmittedJobsCallback callback, final Integer number, final Integer newNumber) {
    return new ResponseHandler() {

    @Override
    public void onResponse(@Nonnull Message response) {
    callback.handle(false, response.errors(), number, newNumber);
    }

    @Override
    public void onMessageProcessingError(@Nonnull String error) {
    LOG.info("Error occurred");
    callback.handle(true, null, number, newNumber);
    }

    @Override
    public void onTimeout() {
    LOG.info("Timeout occurred");
    callback.handle(true, null, number, newNumber);
    }
    };
    }

    private interface GetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer number);
    }

    private interface SetNumberOfSubmittedJobsCallback {
    void handle(boolean responseMissing, ErrorSet errors, Integer oldNumber, Integer newNumber);
    }

    private static void reportServiceIsReady() {
    String lockFile = System.getenv("AVID_SERVICE_LOCK_FILE");
    if (lockFile == null) {
    LOG.info("AVID_SERVICE_LOCK_FILE is not provided");
    return;
    }

    File f = new File(lockFile);
    f.getParentFile().mkdirs();
    try {
    boolean createdNew = f.createNewFile();
    if (!createdNew) {
    LOG.warn("File [{}] was already created", lockFile);
    }
    } catch (IOException e) {
    LOG.info("Failed to create file [{}]", lockFile);
    }
    }
    }
  2. Create etc, etc/init.d, etc/syconfig and etc/logrotate.d directories:

    1
    2
    3
    4
    mkdir etc
    mkdir etc/init.d
    mkdir etc/sysconfig
    mkdir etc/logrotate.d
  3. Create a java-service file in each off the etc/init.d, etc/syconfig and etc/logrotate.d directories:

    1
    2
    3
    touch etc/init.d/java-service
    touch etc/sysconfig/java-service
    touch etc/logrotate.d/java-service
  4. Add the following content to etc/sysconfig/java-service:

    1
    2
    3
    4
    #export ACS_GATEWAY_HOST=127.0.0.1
    #export ACS_GATEWAY_PORT=9900
    #export ACS_BUS_QUERY_TIMEOUT=10000
    #export ACS_LOG_LEVEL=info
  5. Add the following content to etc/logrotate.d/java-service:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /var/log/avid/java-service/java-service.log {
    size 100M
    rotate 10
    copytruncate
    missingok
    notifempty
    compress
    delaycompress
    }
  6. Add the following content to etc/init.d/java-service:

    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
    #!/bin/sh
    #
    # java-service Tutorial Java service
    #
    # chkconfig: - 86 15
    # description: Tutorial service based on Avid Connector API for Java
    #

    ### BEGIN INIT INFO
    # Provides: java-service
    # Required-Start: $local_fs $network $remote_fs
    # Required-Stop: $local_fs $network $remote_fs
    # Short-Description: Tutorial Java service
    # Description: Tutorial service based on Avid Connector API for Java
    #
    ### END INIT INFO

    AVID_SERVICE_NAME="java-service"
    AVID_SERVICE_PID_FILE="/var/run/avid/${AVID_SERVICE_NAME}/${AVID_SERVICE_NAME}.pid"
    AVID_SERVICE_START_CMD="/opt/avid/sbin/${AVID_SERVICE_NAME}"
    AVID_SERVICE_STDOUT_FILE="/var/log/avid/${AVID_SERVICE_NAME}/${AVID_SERVICE_NAME}.log"
    AVID_SERVICE_STDERR_FILE="/var/log/avid/${AVID_SERVICE_NAME}/${AVID_SERVICE_NAME}.err"
    AVID_SERVICE_LOCK_FILE="/var/lock/avid/${AVID_SERVICE_NAME}/${AVID_SERVICE_NAME}.lock"

    # Source service config file if it exists
    [ -e /etc/sysconfig/$AVID_SERVICE_NAME ] && . /etc/sysconfig/$AVID_SERVICE_NAME

    # Source function library.
    . /opt/avid/share/avid-daemonizer/avid-daemonizer.sh

    # Call into the generic case statement in acs-functions
    avid_svc_ctrl "$@"
  7. Modify java-service.spec:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    %global debug_package %{nil}

    Name: java-service
    Version: 2
    Release: 1
    Summary: Tutorial service based on Avid Connector API for Java
    License: Avid EULA
    Vendor: Avid Technology Inc.
    URL: http://avid.com
    Source: %{name}-src.tgz
    BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
    Prefix: /opt/avid

    AutoReqProv: no

    Requires: jre-1.7.0-avid-openjdk-x86_64

    %description
    Tutorial service based on Avid Connector API for Java

    %build
    if [ ! -f module/build/install/%{name}/bin/%{name} ]
    then
    echo "You have to call 'module/gradlew -p module clean installDist' first"
    exit -1
    fi

    %prep
    %setup -c -n %{name}

    %install
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/sbin"
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/bin"
    mkdir -p "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/lib"

    mv etc "${RPM_BUILD_ROOT}/"
    mv module/build/install/java-service/bin/* "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/bin/"
    mv module/build/install/java-service/lib/* "${RPM_BUILD_ROOT}/opt/avid/lib/java_modules/%{name}/lib/"
    ln -s /opt/avid/lib/java_modules/%{name}/bin/%{name} "${RPM_BUILD_ROOT}/opt/avid/sbin/%{name}"

    %post
    chkconfig --add %{name}

    # Exit clean
    exit 0

    %preun
    service %{name} stop ||:

    # If uninstalling, remove chkconfig entry
    if [ $1 -eq 0 ]; then
    chkconfig --del %{name}
    fi

    %postun
    # If upgrading, restart the service if it was already running
    if [ $1 -eq 1 ]; then
    chkconfig %{name} off
    service %{name} condrestart ||:
    fi

    %files
    %attr(755, root, root) /etc/init.d/%{name}
    %attr(644, root, root) /etc/logrotate.d/%{name}
    %attr(644, root, root) /etc/sysconfig/%{name}
    %attr(755, root, root) /opt/avid/lib/java_modules/%{name}/bin/%{name}
    /opt/avid/sbin/%{name}
    /opt/avid/lib/java_modules/%{name}
  8. Build your project:

    1
    module/gradlew -p module clean installDist
  9. Copy your project to avid-platform-zone1 using ssh (username: root; password: vagrant). Your current directory should be java-service:

    1
    scp -r ../java-service root@avid-platform-zone1:/root/
  10. Log in to avid-platform-zone1 with ssh (username: root; password: vagrant):

    1
    ssh root@avid-platform-zone1
  11. In the ssh shell change your current directory:

    1
    cd /root/java-service
  12. If you created files on Windows, make sure to use the LF line separator for bash scripts; if not, convert CRLF to LF with the following command:

    1
    2
    3
    sed -i 's/\r//' create-rpm.sh
    sed -i 's/\r//' etc/init.d/node-service
    sed -i 's/\r//' module/bin/node-service.js

    Or, you can use vi or vim editor to change the file format by opening the file in the editor and then use :set ff=unix and then save the file.

  13. Create the RPM:

    1
    ./create-rpm.sh java-service.spec
  14. Install your RPM:

    1
    yum install rpmbuild/RPMS/x86_64/java-service-2-1.x86_64.rpm
  15. Run the installed service:

    1
    service java-service start
  16. Test that the service is working using the ACS Monitor as previously outlined.

  17. Monitor the service logs:

    1
    2
    3
    service java-service logs
    #or
    tail -f /var/log/avid/java-service/java-service.log

Congratulations! You have successfully completed the tutorial.