[GRIFFIN-229] trigger the job right now with fixing comments

[comment](https://github.com/apache/griffin/pull/480#issuecomment-460075951)
> dyingbleed can you please include screenshots for UI changes?

<img width="1074" alt="screen shot 2019-03-06 at 18 19 25" src="https://user-images.githubusercontent.com/43031807/53944639-d4424980-40d0-11e9-977e-29b45b879bf9.png">
<img width="1065" alt="screen shot 2019-03-06 at 18 43 23" src="https://user-images.githubusercontent.com/43031807/53944659-db695780-40d0-11e9-9114-43879cf068fa.png">

Author: Borgatin Alexandr <aborgatin@griddynamics.com>
Author: Alexandr Borgatin <43031807+aborgatin@users.noreply.github.com>
Author: Anthony Li <lizhen.dgut@gmail.com>

Closes #485 from aborgatin/feature/GRIFFIN-229.
diff --git a/griffin-doc/service/api-guide.md b/griffin-doc/service/api-guide.md
index 30bbcc7..95113af 100644
--- a/griffin-doc/service/api-guide.md
+++ b/griffin-doc/service/api-guide.md
@@ -36,6 +36,7 @@
 
 - [Griffin Jobs](#3)
     - [Add Job](#31)
+    - [Trigger job by id](37)
     - [Get Job](#32)
     - [Remove Job](#33)
     - [Get Job Instances](#34)
@@ -542,6 +543,15 @@
 }'
 ```
 
+<div id = "37"></div>
+
+### Trigger job by id
+`POST /api/v1/jobs/trigger/{job_id}`
+#### API Example
+```
+curl -k -X POST http://127.0.0.1:8080/api/v1/jobs/trigger/51
+```
+
 <div id = "32"></div>
 
 ### Get all jobs
diff --git a/griffin-doc/service/postman/griffin.json b/griffin-doc/service/postman/griffin.json
index ac64412..e789121 100644
--- a/griffin-doc/service/postman/griffin.json
+++ b/griffin-doc/service/postman/griffin.json
@@ -1592,6 +1592,38 @@
 					]
 				},
 				{
+					"name": "Trigger job by id",
+					"request": {
+						"method": "POST",
+						"header": [],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{BASE_PATH}}/api/v1/jobs/trigger/:id",
+							"host": [
+								"{{BASE_PATH}}"
+							],
+							"path": [
+								"api",
+								"v1",
+								"jobs",
+								"trigger",
+								":id"
+							],
+							"variable": [
+								{
+									"key": "id",
+									"value": ""
+								}
+							]
+						},
+						"description": "`POST /api/v1/jobs/trigger/{id}`\n\n#### Path Variable\n- id -`required` `Long` job id\n\n#### Response\nThe response body should be empty if no error happens, and the HTTP status is (204, \"No Content\").\n\nIt may return failed messages. For example\n```\n{\n    \"timestamp\": 1517208792108,\n    \"status\": 404,\n    \"error\": \"Not Found\",\n    \"code\": 40402,\n    \"message\": \"Job id does not exist\",\n    \"path\": \"/api/v1/jobs/trigger/2\"\n}\n```\nThere will be 'status' and 'error' fields in response if error happens, which correspond to HTTP status.\n\nThere may also be 'code' and 'message' fields, which will point out the cause.\n\nIf an exception happens at server, there will be an 'exception' field, which is the name of exception."
+					},
+					"response": []
+				},
+				{
 					"name": "Delete  job by name",
 					"request": {
 						"method": "DELETE",
diff --git a/service/src/main/java/org/apache/griffin/core/job/JobController.java b/service/src/main/java/org/apache/griffin/core/job/JobController.java
index f4ee791..b5274a8 100644
--- a/service/src/main/java/org/apache/griffin/core/job/JobController.java
+++ b/service/src/main/java/org/apache/griffin/core/job/JobController.java
@@ -113,4 +113,10 @@
                 .contentType(MediaType.APPLICATION_OCTET_STREAM)
                 .body(resource);
     }
+
+    @RequestMapping(value = "/jobs/trigger/{id}", method = RequestMethod.POST)
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void triggerJob(@PathVariable("id") Long id) throws SchedulerException {
+        jobService.triggerJobById(id);
+    }
 }
diff --git a/service/src/main/java/org/apache/griffin/core/job/JobService.java b/service/src/main/java/org/apache/griffin/core/job/JobService.java
index 58e541f..42415d9 100644
--- a/service/src/main/java/org/apache/griffin/core/job/JobService.java
+++ b/service/src/main/java/org/apache/griffin/core/job/JobService.java
@@ -45,4 +45,6 @@
     JobHealth getHealthInfo();
 
     String getJobHdfsSinksPath(String jobName, long timestamp);
+
+    void triggerJobById(Long id) throws SchedulerException;
 }
diff --git a/service/src/main/java/org/apache/griffin/core/job/JobServiceImpl.java b/service/src/main/java/org/apache/griffin/core/job/JobServiceImpl.java
index 7dc7f6d..a7fc546 100644
--- a/service/src/main/java/org/apache/griffin/core/job/JobServiceImpl.java
+++ b/service/src/main/java/org/apache/griffin/core/job/JobServiceImpl.java
@@ -45,14 +45,7 @@
 import org.apache.griffin.core.util.YarnNetUtil;
 import org.json.JSONArray;
 import org.json.JSONObject;
-import org.quartz.JobDataMap;
-import org.quartz.JobDetail;
-import org.quartz.JobKey;
-import org.quartz.Scheduler;
-import org.quartz.SchedulerException;
-import org.quartz.Trigger;
-import org.quartz.TriggerBuilder;
-import org.quartz.TriggerKey;
+import org.quartz.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -99,7 +92,7 @@
 import static org.quartz.CronScheduleBuilder.cronSchedule;
 import static org.quartz.JobBuilder.newJob;
 import static org.quartz.JobKey.jobKey;
-import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
+import static org.quartz.SimpleScheduleBuilder.*;
 import static org.quartz.TriggerBuilder.newTrigger;
 import static org.quartz.TriggerKey.triggerKey;
 
@@ -661,4 +654,21 @@
             return null;
         }
     }
+
+    @Override
+    public void triggerJobById(Long id) throws SchedulerException {
+        AbstractJob job = jobRepo.findByIdAndDeleted(id, false);
+        validateJobExist(job);
+        Scheduler scheduler = factory.getScheduler();
+        JobKey jobKey = jobKey(job.getName(), job.getGroup());
+        if (scheduler.checkExists(jobKey)) {
+            Trigger trigger = TriggerBuilder.newTrigger()
+                    .forJob(jobKey)
+                    .startNow()
+                    .build();
+            scheduler.scheduleJob(trigger);
+        } else {
+            LOGGER.warn("Could not trigger job id {}.", id);
+        }
+    }
 }
diff --git a/service/src/test/java/org/apache/griffin/core/job/JobControllerTest.java b/service/src/test/java/org/apache/griffin/core/job/JobControllerTest.java
index 0bd74e6..ab39b16 100644
--- a/service/src/test/java/org/apache/griffin/core/job/JobControllerTest.java
+++ b/service/src/test/java/org/apache/griffin/core/job/JobControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.doThrow;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -171,4 +172,22 @@
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.healthyJobCount", is(1)));
     }
+
+    @Test
+    public void testTriggerJobForSuccess() throws Exception {
+        doNothing().when(service).triggerJobById(1L);
+
+        mvc.perform(post(URLHelper.API_VERSION_PATH + "/jobs/trigger/1"))
+                .andExpect(status().isNoContent());
+    }
+
+    @Test
+    public void testTriggerJobForFailureWithException() throws Exception {
+        doThrow(new GriffinException.ServiceException("Failed to trigger job",
+                new Exception()))
+                .when(service).triggerJobById(1L);
+
+        mvc.perform(post(URLHelper.API_VERSION_PATH + "/jobs/trigger/1"))
+                .andExpect(status().isInternalServerError());
+    }
 }
diff --git a/service/src/test/java/org/apache/griffin/core/job/JobServiceImplTest.java b/service/src/test/java/org/apache/griffin/core/job/JobServiceImplTest.java
new file mode 100644
index 0000000..a335de2
--- /dev/null
+++ b/service/src/test/java/org/apache/griffin/core/job/JobServiceImplTest.java
@@ -0,0 +1,56 @@
+package org.apache.griffin.core.job;
+
+import org.apache.griffin.core.exception.GriffinException;
+import org.apache.griffin.core.job.entity.AbstractJob;
+import org.apache.griffin.core.job.repo.JobRepo;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.springframework.scheduling.quartz.SchedulerFactoryBean;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.apache.griffin.core.util.EntityMocksHelper.createGriffinJob;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.internal.verification.VerificationModeFactory.times;
+
+@RunWith(SpringRunner.class)
+public class JobServiceImplTest {
+
+    @Mock
+    private JobRepo<AbstractJob> jobRepo;
+
+    @Mock
+    private SchedulerFactoryBean factory;
+
+    @InjectMocks
+    private JobServiceImpl jobService;
+
+
+    @Test
+    public void testTriggerJobById() throws SchedulerException {
+        Long jobId = 1L;
+        AbstractJob job = createGriffinJob();
+        given(jobRepo.findByIdAndDeleted(jobId,false)).willReturn(job);
+        Scheduler scheduler = mock(Scheduler.class);
+        given(scheduler.checkExists(any(JobKey.class))).willReturn(true);
+        given(factory.getScheduler()).willReturn(scheduler);
+        jobService.triggerJobById(jobId);
+
+        verify(scheduler, times(1)).scheduleJob(any());
+    }
+
+
+    @Test(expected = GriffinException.NotFoundException.class)
+    public void testTriggerJobByIdFail() throws SchedulerException {
+        Long jobId = 1L;
+        given(jobRepo.findByIdAndDeleted(jobId,false)).willReturn(null);
+        jobService.triggerJobById(jobId);
+    }
+}
diff --git a/ui/angular/src/app/job/job.component.html b/ui/angular/src/app/job/job.component.html
index 2fb87a2..2b7cd9d 100644
--- a/ui/angular/src/app/job/job.component.html
+++ b/ui/angular/src/app/job/job.component.html
@@ -77,7 +77,7 @@
           &nbsp;
           <a (click)="remove(row)" title="delete" style="text-decoration:none">
             <i class="fa fa-trash-o po"></i>
-          </a> &nbsp;
+          </a>&nbsp;
           <a routerLink="/job/{{row.id}}" title="subscribe">
             <i class="fa fa-eye"></i>
           </a>&nbsp;
@@ -86,6 +86,9 @@
           </a>
           <a *ngIf="row.action!=='START'" (click)="stateMag(row)" title="Stop" style="text-decoration:none">
             <i class="fa fa-stop"></i>
+          </a>&nbsp;
+          <a (click)="trigger(row)" title="trigger now" style="text-decoration:none">
+            <i class="fa fa-caret-square-o-right po"></i>
           </a>
         </td>
         <td>
diff --git a/ui/angular/src/app/job/job.component.ts b/ui/angular/src/app/job/job.component.ts
index 0a86fe5..893db64 100644
--- a/ui/angular/src/app/job/job.component.ts
+++ b/ui/angular/src/app/job/job.component.ts
@@ -45,6 +45,7 @@
   action: string;
   modalWndMsg: string;
   isStop: boolean;
+  isTrigger: boolean;
 
   private toasterService: ToasterService;
 
@@ -101,6 +102,19 @@
           console.log("Error when manage job state");
         });
     }
+    else if (this.isTrigger) {
+      $("#save").attr("disabled", "true");
+      let actionUrl = this.serviceService.config.uri.triggerJobById + "/" + this.deleteId;
+      this.http.post(actionUrl, {}).subscribe(data => {
+          let self = this;
+          self.hide();
+          this.isTrigger = false;
+        },
+        err => {
+          this.toasterService.pop("error", "Error!", "Failed to trigger job!");
+          console.log("Error when trigger job");
+        });
+    }
     else {
       let deleteJob = this.serviceService.config.uri.deleteJob;
       let deleteUrl = deleteJob + "/" + this.deleteId;
@@ -196,4 +210,15 @@
       this.results = Object.assign([], trans).reverse();
     });
   }
+
+  trigger(row): void {
+    $("#save").removeAttr("disabled");
+    this.modalWndMsg = "Trigger the job with the below information?";
+    this.visible = true;
+    setTimeout(() => (this.visibleAnimate = true), 100);
+    this.deletedRow = row;
+    this.deleteIndex = this.results.indexOf(row);
+    this.deleteId = row.id;
+    this.isTrigger = true;
+  }
 }
diff --git a/ui/angular/src/app/service/service.service.ts b/ui/angular/src/app/service/service.service.ts
index 7d50b4b..57093f4 100644
--- a/ui/angular/src/app/service/service.service.ts
+++ b/ui/angular/src/app/service/service.service.ts
@@ -92,6 +92,7 @@
       addJobs: this.BACKEND_SERVER + this.API_ROOT_PATH + "/jobs",
       modifyJobs: this.BACKEND_SERVER + this.API_ROOT_PATH + "/jobs",
       getJobById: this.BACKEND_SERVER + this.API_ROOT_PATH + "/jobs/config",
+      triggerJobById: this.BACKEND_SERVER + this.API_ROOT_PATH + "/jobs/trigger",
       getMeasuresByOwner:
       this.BACKEND_SERVER + this.API_ROOT_PATH + "/measures/owner/",
       deleteJob: this.BACKEND_SERVER + this.API_ROOT_PATH + "/jobs",