JAL-3851 merged to develop 2022-03-22
[jalview.git] / src / jalview / rest / AbstractEndpointAsync.java
1 package jalview.rest;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.HashMap;
6 import java.util.Map;
7 import java.util.concurrent.CompletableFuture;
8
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11
12 import jalview.bin.Console;
13 import jalview.rest.RestHandler.Status;
14
15 public abstract class AbstractEndpointAsync extends AbstractEndpoint
16 {
17   public AbstractEndpointAsync(API api, String path, String name,
18           String parameters, String description)
19   {
20     super(api, path, name, parameters, description);
21   }
22
23   protected String idExtension = null;
24
25   protected String id = null;
26
27   protected CompletableFuture<Void> cf = null;
28
29   protected Map<String, CompletableFuture<Void>> cfMap = new HashMap<>();
30
31   protected Map<String, Object> objectsPassedToProcessAsync = new HashMap<>();
32
33   private Status tempStatus = null;
34
35   protected void setCompletableFuture(CompletableFuture<Void> cf)
36   {
37     this.cf = cf;
38     if (getId() != null)
39       cfMap.put(getId(), cf);
40   }
41
42   protected CompletableFuture<Void> getCompletableFuture()
43   {
44     if (cf == null && getId() != null && cfMap.get(getId()) != null)
45       cf = cfMap.get(getId());
46     return this.cf;
47   }
48
49   protected void setId(String id)
50   {
51     this.id = id;
52   }
53
54   protected void setIdExtension(String idExtension)
55   {
56     setId(getPath() + "::" + idExtension);
57   }
58
59   protected String getId()
60   {
61     return this.id;
62   }
63
64   /*
65    * Override the three methods
66    * initialise (get parameters, set id (extension), set cf if using an existing one)
67    * process (what to do in the cf if not using an existing one)
68    * finalise (extra stuff to do at the end of the first call to this)
69    */
70   protected void initialise(HttpServletRequest request,
71           HttpServletResponse response)
72   {
73     // should be overridden
74     // must setId(request, extension)
75     setId(request, null);
76   }
77
78   protected abstract void processAsync(HttpServletRequest request,
79           HttpServletResponse response, final Map<String, Object> finalMap);
80
81   protected void finalise(HttpServletRequest request,
82           HttpServletResponse response)
83   {
84     // can be Overridden
85   }
86
87   @Override
88   public void processEndpoint(HttpServletRequest request,
89           HttpServletResponse response)
90   {
91     tempStatus = null;
92     // subclass method
93     initialise(request, response);
94
95     if (checkStatus(request, response, Status.STARTED))
96     {
97       String alreadyFinishedString = null;
98       if (getStatus() == Status.FINISHED)
99       {
100         alreadyFinishedString = finishedResponseString(request, response);
101       }
102       returnStatus(request, response, alreadyFinishedString);
103       return;
104     }
105
106     if (getCompletableFuture() == null)
107     {
108       final Map<String, Object> finalObjectMap = objectsPassedToProcessAsync;
109       setCompletableFuture(CompletableFuture.runAsync(() -> {
110         // subclass method
111         try
112         {
113           this.processAsync(request, response, finalObjectMap);
114         } catch (ClassCastException e)
115         {
116           Console.info("Something went wrong with async endpoint execution"
117                   + getName(), e);
118         }
119       }));
120     }
121     addWhenCompleteCompletableFuture();
122
123     // subclass method
124     finalise(request, response);
125
126     returnStatus(response);
127     changeStatus(Status.IN_PROGRESS);
128   }
129
130   protected void atEnd()
131   {
132   }
133
134   protected String finishedResponseString(HttpServletRequest request,
135           HttpServletResponse response)
136   {
137     return null;
138   }
139
140   /*
141    * Shared methods below here
142    */
143
144   protected String setId(HttpServletRequest request, String extension)
145   {
146     String idString = request.getParameter("id");
147     if (idString == null)
148     {
149       setIdExtension(extension);
150     }
151     else
152     {
153       setId(idString);
154     }
155     return getId();
156   }
157
158   protected void changeStatus(Status status)
159   {
160     String id = getId();
161     // don't change a job's status if it has finished or died
162     if (getStatus() == Status.FINISHED || getStatus() == Status.ERROR)
163       return;
164     tempStatus = status;
165     if (status != Status.NOT_RUN)
166       API.getStatusMap().put(id, status);
167   }
168
169   protected Status getStatus()
170   {
171     Status status = API.getStatusMap().get(getId());
172     return status == null ? tempStatus : status;
173   }
174
175   protected void returnStatus(HttpServletResponse response)
176   {
177     returnStatus(null, response, null);
178   }
179
180   protected void returnStatus(HttpServletRequest request,
181           HttpServletResponse response, String message)
182   {
183     String id = getId();
184     try
185     {
186       PrintWriter writer = response.getWriter();
187       if (id != null)
188       {
189         writer.write("id=" + id + "\n");
190       }
191       if (API.getRequestMap().get(id) != null)
192       {
193         writer.write(
194                 "request=" + API.getRequestMap().get(id).toString() + "\n");
195       }
196       if (getStatus() != null)
197       {
198         switch (getStatus())
199         {
200         case STARTED:
201           response.setStatus(HttpServletResponse.SC_ACCEPTED);
202           break;
203         case IN_PROGRESS:
204           response.setStatus(HttpServletResponse.SC_ACCEPTED);
205           break;
206         case FINISHED:
207           response.setStatus(HttpServletResponse.SC_CREATED);
208           break;
209         case ERROR:
210           response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
211                   message);
212           break;
213         }
214         writer.write("status=" + getStatus().toString() + "\n");
215       }
216       if (message != null)
217       {
218         writer.write(message);
219       }
220     } catch (IOException e)
221     {
222       Console.debug("Exception writing to REST response", e);
223     }
224   }
225
226   protected boolean checkStatus(HttpServletRequest request,
227           HttpServletResponse response)
228   {
229     return checkStatus(request, response, null);
230   }
231
232   protected boolean checkStatus(HttpServletRequest request,
233           HttpServletResponse response, Status set)
234   {
235     String id = getId();
236     Status status = getStatus();
237     if (status == null)
238     {
239       if (set != null)
240         changeStatus(set);
241       API.getRequestMap().put(id, request.getRequestURI());
242       return false;
243     }
244     else
245     {
246       return true;
247     }
248   }
249
250   protected void addWhenCompleteCompletableFuture()
251   {
252     String id = getId();
253     cf.whenComplete((Void, e) -> {
254       if (e != null)
255       {
256         Console.error("Endpoint job " + id + " did not complete", e);
257         changeStatus(Status.ERROR);
258       }
259       else
260       {
261         Console.info("Endpoint job " + id + " completed successfully");
262         changeStatus(Status.FINISHED);
263         atEnd();
264       }
265     });
266   }
267
268   @Override
269   protected void returnError(HttpServletRequest request,
270           HttpServletResponse response, String message)
271   {
272     changeStatus(Status.NOT_RUN);
273     super.returnError(request, response, message);
274   }
275
276   @Override
277   protected boolean deleteFromCache()
278   {
279     return false;
280   }
281 }