1
|
// $Id$
|
2
|
/**
|
3
|
* Copyright (C) 2014 EDIT
|
4
|
* European Distributed Institute of Taxonomy
|
5
|
* http://www.e-taxonomy.eu
|
6
|
*
|
7
|
* The contents of this file are subject to the Mozilla Public License Version 1.1
|
8
|
* See LICENSE.TXT at the top of this package for the full license terms.
|
9
|
*/
|
10
|
package org.bgbm.utis.controller;
|
11
|
|
12
|
import java.io.IOException;
|
13
|
import java.math.BigDecimal;
|
14
|
import java.util.ArrayList;
|
15
|
import java.util.Collection;
|
16
|
import java.util.HashMap;
|
17
|
import java.util.HashSet;
|
18
|
import java.util.List;
|
19
|
import java.util.Map;
|
20
|
import java.util.Set;
|
21
|
import java.util.regex.Matcher;
|
22
|
import java.util.regex.Pattern;
|
23
|
|
24
|
import javax.servlet.http.HttpServletRequest;
|
25
|
import javax.servlet.http.HttpServletResponse;
|
26
|
|
27
|
import org.cybertaxonomy.utis.checklist.BaseChecklistClient;
|
28
|
import org.cybertaxonomy.utis.checklist.BgbmEditClient;
|
29
|
import org.cybertaxonomy.utis.checklist.DRFChecklistException;
|
30
|
import org.cybertaxonomy.utis.checklist.PESIClient;
|
31
|
import org.cybertaxonomy.utis.checklist.SearchMode;
|
32
|
import org.cybertaxonomy.utis.checklist.WoRMSClient;
|
33
|
import org.cybertaxonomy.utis.client.AbstractClient;
|
34
|
import org.cybertaxonomy.utis.client.ServiceProviderInfo;
|
35
|
import org.cybertaxonomy.utis.tnr.msg.Query;
|
36
|
import org.cybertaxonomy.utis.tnr.msg.Query.ClientStatus;
|
37
|
import org.cybertaxonomy.utis.tnr.msg.Response;
|
38
|
import org.cybertaxonomy.utis.tnr.msg.TnrMsg;
|
39
|
import org.cybertaxonomy.utis.utils.TnrMsgUtils;
|
40
|
import org.slf4j.Logger;
|
41
|
import org.slf4j.LoggerFactory;
|
42
|
import org.springframework.beans.factory.config.BeanDefinition;
|
43
|
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
44
|
import org.springframework.core.type.filter.AssignableTypeFilter;
|
45
|
import org.springframework.http.HttpStatus;
|
46
|
import org.springframework.stereotype.Controller;
|
47
|
import org.springframework.web.bind.annotation.RequestMapping;
|
48
|
import org.springframework.web.bind.annotation.RequestMethod;
|
49
|
import org.springframework.web.bind.annotation.RequestParam;
|
50
|
import org.springframework.web.bind.annotation.ResponseBody;
|
51
|
|
52
|
import com.fasterxml.jackson.core.JsonGenerationException;
|
53
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
54
|
import com.wordnik.swagger.annotations.ApiParam;
|
55
|
|
56
|
/**
|
57
|
* @author a.kohlbecker
|
58
|
* @date Jun 27, 2014
|
59
|
*
|
60
|
*/
|
61
|
|
62
|
@Controller
|
63
|
@RequestMapping(produces={"application/json","application/xml"}) // produces is needed for swagger)
|
64
|
public class UtisController {
|
65
|
|
66
|
protected Logger logger = LoggerFactory.getLogger(UtisController.class);
|
67
|
|
68
|
private Map<String, ServiceProviderInfo> serviceProviderInfoMap;
|
69
|
private Map<String, Class<? extends BaseChecklistClient>> clientClassMap;
|
70
|
|
71
|
private final List<ServiceProviderInfo> defaultProviders = new ArrayList<ServiceProviderInfo>();
|
72
|
|
73
|
public UtisController() throws ClassNotFoundException {
|
74
|
initProviderMap();
|
75
|
}
|
76
|
|
77
|
|
78
|
public static <T extends AbstractClient> Set<Class<T>> subclassesFor(Class<T> clazz) throws ClassNotFoundException{
|
79
|
|
80
|
Set<Class<T>> subClasses = new HashSet<Class<T>>();
|
81
|
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true);
|
82
|
provider.addIncludeFilter(new AssignableTypeFilter(clazz));
|
83
|
|
84
|
// scan only in org.cybertaxonomy.utis
|
85
|
Set<BeanDefinition> components = provider.findCandidateComponents("org/cybertaxonomy/utis/");
|
86
|
for (BeanDefinition component : components)
|
87
|
{
|
88
|
subClasses.add((Class<T>) Class.forName(component.getBeanClassName()));
|
89
|
}
|
90
|
return subClasses;
|
91
|
}
|
92
|
|
93
|
/**
|
94
|
* @throws ClassNotFoundException
|
95
|
*
|
96
|
*/
|
97
|
private void initProviderMap() throws ClassNotFoundException {
|
98
|
|
99
|
Set<Class<BaseChecklistClient>> checklistClients;
|
100
|
checklistClients = subclassesFor(BaseChecklistClient.class);
|
101
|
|
102
|
serviceProviderInfoMap = new HashMap<String, ServiceProviderInfo>();
|
103
|
clientClassMap = new HashMap<String, Class<? extends BaseChecklistClient>>();
|
104
|
|
105
|
for(Class<BaseChecklistClient> clientClass: checklistClients){
|
106
|
|
107
|
BaseChecklistClient client;
|
108
|
try {
|
109
|
client = clientClass.newInstance();
|
110
|
ServiceProviderInfo info = client.buildServiceProviderInfo();
|
111
|
|
112
|
clientClassMap.put(info.getId(), clientClass);
|
113
|
info.setSearchModes(client.getSearchModes()); // TODO setSearchModes should be done in client impl
|
114
|
serviceProviderInfoMap.put(info.getId(), info);
|
115
|
|
116
|
} catch (InstantiationException e) {
|
117
|
// TODO Auto-generated catch block
|
118
|
e.printStackTrace();
|
119
|
} catch (IllegalAccessException e) {
|
120
|
// TODO Auto-generated catch block
|
121
|
e.printStackTrace();
|
122
|
}
|
123
|
}
|
124
|
|
125
|
defaultProviders.add(serviceProviderInfoMap.get(PESIClient.ID));
|
126
|
defaultProviders.add(serviceProviderInfoMap.get(BgbmEditClient.ID));
|
127
|
defaultProviders.add(serviceProviderInfoMap.get(WoRMSClient.ID));
|
128
|
}
|
129
|
|
130
|
/**
|
131
|
* @param providers
|
132
|
* @param response
|
133
|
* @return
|
134
|
* @throws IOException
|
135
|
*/
|
136
|
private List<ServiceProviderInfo> createProviderList(String providers, HttpServletResponse response)
|
137
|
throws IOException {
|
138
|
List<ServiceProviderInfo> providerList = defaultProviders;
|
139
|
if (providers != null) {
|
140
|
String[] providerIdTokens = providers.split(",");
|
141
|
providerList = new ArrayList<ServiceProviderInfo>();
|
142
|
for (String id : providerIdTokens) {
|
143
|
|
144
|
List<String> subproviderIds = parsSubproviderIds(id);
|
145
|
if(!subproviderIds.isEmpty()){
|
146
|
id = id.substring(0, id.indexOf("["));
|
147
|
}
|
148
|
|
149
|
if(serviceProviderInfoMap.containsKey(id)){
|
150
|
ServiceProviderInfo provider = serviceProviderInfoMap.get(id);
|
151
|
if(!subproviderIds.isEmpty()){
|
152
|
Collection<ServiceProviderInfo> removeCandidates = new ArrayList<ServiceProviderInfo>();
|
153
|
for(ServiceProviderInfo subProvider : provider.getSubChecklists()){
|
154
|
if(!subproviderIds.contains(subProvider.getId())){
|
155
|
removeCandidates.add(subProvider);
|
156
|
}
|
157
|
}
|
158
|
provider.getSubChecklists().removeAll(removeCandidates);
|
159
|
}
|
160
|
providerList.add(provider);
|
161
|
}
|
162
|
}
|
163
|
if(providerList.isEmpty()){
|
164
|
response.sendError(HttpStatus.BAD_REQUEST.value(), "invalid value for request parameter 'providers' given: " + defaultProviders.toString());
|
165
|
throw new IllegalArgumentException("invalid value for request parameter 'providers' given: " + defaultProviders.toString());
|
166
|
}
|
167
|
}
|
168
|
return providerList;
|
169
|
}
|
170
|
|
171
|
|
172
|
private List<String> parsSubproviderIds(String id) {
|
173
|
|
174
|
List<String> subIds = new ArrayList<String>();
|
175
|
Pattern pattern = Pattern.compile("^.*\\[([\\w,]*)\\]$");
|
176
|
|
177
|
Matcher m = pattern.matcher(id);
|
178
|
if (m.matches()) {
|
179
|
String subids = m.group(1);
|
180
|
String[] subidTokens = subids.split(",");
|
181
|
for (String subId : subidTokens) {
|
182
|
subIds.add(subId);
|
183
|
}
|
184
|
}
|
185
|
return subIds;
|
186
|
}
|
187
|
|
188
|
|
189
|
private BaseChecklistClient newClientFor(String id) {
|
190
|
|
191
|
BaseChecklistClient instance = null;
|
192
|
|
193
|
if(!clientClassMap.containsKey(id)){
|
194
|
logger.error("Unsupported Client ID: "+ id);
|
195
|
|
196
|
} else {
|
197
|
try {
|
198
|
instance = clientClassMap.get(id).newInstance();
|
199
|
} catch (InstantiationException e) {
|
200
|
// TODO Auto-generated catch block
|
201
|
e.printStackTrace();
|
202
|
} catch (IllegalAccessException e) {
|
203
|
// TODO Auto-generated catch block
|
204
|
e.printStackTrace();
|
205
|
}
|
206
|
}
|
207
|
|
208
|
return instance;
|
209
|
}
|
210
|
|
211
|
@RequestMapping(method = { RequestMethod.GET }, value = "/capabilities")
|
212
|
public @ResponseBody List<ServiceProviderInfo> capabilities(HttpServletRequest request, HttpServletResponse response) {
|
213
|
return defaultProviders;
|
214
|
}
|
215
|
|
216
|
|
217
|
/**
|
218
|
*
|
219
|
* @param queryString The complete canonical scientific name to search for. For
|
220
|
* example: <code>Bellis perennis</code>, <code>Prionus</code> or
|
221
|
* <code>Bolinus brandaris</code>.
|
222
|
* This is a exact search so wildcard characters are not supported.
|
223
|
*
|
224
|
* @param providers
|
225
|
* A list of provider id strings concatenated by comma
|
226
|
* characters. The default : <code>pesi,edit</code> will be used
|
227
|
* if this parameter is not set. A list of all available provider
|
228
|
* ids can be obtained from the <code>/capabilities</code> service
|
229
|
* end point.
|
230
|
* @param request
|
231
|
* @param response
|
232
|
* @return
|
233
|
* @throws DRFChecklistException
|
234
|
* @throws JsonGenerationException
|
235
|
* @throws JsonMappingException
|
236
|
* @throws IOException
|
237
|
*/
|
238
|
@RequestMapping(method = { RequestMethod.GET }, value = "/search")
|
239
|
public @ResponseBody
|
240
|
TnrMsg search(
|
241
|
@ApiParam(
|
242
|
value = "The scientific name to search for. "
|
243
|
+"For example: \"Bellis perennis\", \"Prionus\" or \"Bolinus brandaris\". "
|
244
|
+"This is an exact search so wildcard characters are not supported."
|
245
|
,required=true)
|
246
|
@RequestParam(value = "query", required = false)
|
247
|
String queryString,
|
248
|
@ApiParam(value = "A list of provider id strings concatenated by comma "
|
249
|
+"characters. The default : \"pesi,bgbm-cdm-server[col]\" will be used "
|
250
|
+ "if this parameter is not set. A list of all available provider "
|
251
|
+"ids can be obtained from the '/capabilities' service "
|
252
|
+"end point. "
|
253
|
+ "Providers can be nested, that is a parent provider can have "
|
254
|
+ "sub providers. If the id of the parent provider is supplied all subproviders will "
|
255
|
+ "be queried. The query can also be restriced to one or more subproviders by "
|
256
|
+ "using the following syntax: parent-id[sub-id-1,sub-id2,...]",
|
257
|
defaultValue="pesi,bgbm-cdm-server[col]",
|
258
|
required=false)
|
259
|
@RequestParam(value = "providers", required = false)
|
260
|
String providers,
|
261
|
@ApiParam(value = "Specifies the searchMode. "
|
262
|
+ "Possible search modes are: scientificNameExact, scientificNameLike (begins with), vernacularNameExact, "
|
263
|
+ "vernacularNameLike (contains), findByIdentifier. "
|
264
|
+ "If the a provider does not support the chosen searchMode it will be skipped and "
|
265
|
+ "the status message in the tnrClientStatus will be set to 'unsupported search mode' in this case.")
|
266
|
@RequestParam(value = "searchMode", required = false, defaultValue="scientificNameExact")
|
267
|
SearchMode searchMode,
|
268
|
@ApiParam(value = "Indicates whether the synonymy of the accepted taxon should be included into the response. "
|
269
|
+ "Turning this option on may cause an increased response time.")
|
270
|
@RequestParam(value = "addSynonymy", required = false, defaultValue="false")
|
271
|
Boolean addSynonymy,
|
272
|
@ApiParam(value = "The maximum of milliseconds to wait for responses from any of the providers. "
|
273
|
+ "If the timeout is exceeded the service will jut return the resonses that have been "
|
274
|
+ "received so far. The default timeout is 0 ms (wait for ever)")
|
275
|
@RequestParam(value = "timeout", required = false, defaultValue="0")
|
276
|
Long timeout,
|
277
|
HttpServletRequest request,
|
278
|
HttpServletResponse response
|
279
|
) throws DRFChecklistException, JsonGenerationException, JsonMappingException,
|
280
|
IOException {
|
281
|
|
282
|
|
283
|
List<ServiceProviderInfo> providerList = createProviderList(providers, response);
|
284
|
|
285
|
TnrMsg tnrMsg = TnrMsgUtils.convertStringToTnrMsg(queryString, searchMode, addSynonymy);
|
286
|
|
287
|
// query all providers
|
288
|
List<ChecklistClientRunner> runners = new ArrayList<ChecklistClientRunner>(providerList.size());
|
289
|
for (ServiceProviderInfo info : providerList) {
|
290
|
BaseChecklistClient client = newClientFor(info.getId());
|
291
|
if(client != null){
|
292
|
logger.debug("sending query to " + info.getId());
|
293
|
ChecklistClientRunner runner = new ChecklistClientRunner(client, tnrMsg);
|
294
|
runner.start();
|
295
|
runners.add(runner);
|
296
|
}
|
297
|
}
|
298
|
|
299
|
// wait for the responses
|
300
|
logger.debug("All runners started, now waiting for them to complete ...");
|
301
|
for(ChecklistClientRunner runner : runners){
|
302
|
try {
|
303
|
logger.debug("waiting for client runner '" + runner.getClient());
|
304
|
runner.join(timeout);
|
305
|
} catch (InterruptedException e) {
|
306
|
logger.debug("client runner '" + runner.getClient() + "' was interrupted", e);
|
307
|
}
|
308
|
}
|
309
|
logger.debug("end of waiting (all runners completed or timed out)");
|
310
|
|
311
|
// collect, re-order the responses and set the status
|
312
|
Query currentQuery = tnrMsg.getQuery().get(0); // TODO HACK: we only are treating one query
|
313
|
List<Response> tnrResponses = currentQuery.getResponse();
|
314
|
List<Response> tnrResponsesOrderd = new ArrayList<Response>(tnrResponses.size());
|
315
|
|
316
|
for(ChecklistClientRunner runner : runners){
|
317
|
ServiceProviderInfo info = runner.getClient().getServiceProviderInfo();
|
318
|
ClientStatus tnrStatus = TnrMsgUtils.tnrClientStatusFor(info);
|
319
|
Response tnrResponse = null;
|
320
|
|
321
|
// --- handle all exception states and create one tnrResonse which will contain the status
|
322
|
if(runner.isInterrupted()){
|
323
|
logger.debug("client runner '" + runner.getClient() + "' was interrupted");
|
324
|
tnrStatus.setStatusMessage("interrupted");
|
325
|
}
|
326
|
else
|
327
|
if(runner.isAlive()){
|
328
|
logger.debug("client runner '" + runner.getClient() + "' has timed out");
|
329
|
tnrStatus.setStatusMessage("timeout");
|
330
|
}
|
331
|
else
|
332
|
if(runner.isUnsupportedMode()){
|
333
|
logger.debug("client runner '" + runner.getClient() + "' : unsupported search mode");
|
334
|
tnrStatus.setStatusMessage("unsupported search mode");
|
335
|
}
|
336
|
else
|
337
|
if(runner.isUnsupportedIdentifier()){
|
338
|
logger.debug("client runner '" + runner.getClient() + "' : identifier type not supported");
|
339
|
tnrStatus.setStatusMessage("identifier type not supported");
|
340
|
}
|
341
|
else {
|
342
|
|
343
|
tnrStatus.setStatusMessage("ok");
|
344
|
// --- collect the ServiceProviderInfo objects by which the responses will be ordered
|
345
|
List<ServiceProviderInfo> ServiceProviderInfos;
|
346
|
if(info.getSubChecklists() != null && !info.getSubChecklists().isEmpty()){
|
347
|
// for subchecklists we will have to look for responses of each of the subchecklists
|
348
|
ServiceProviderInfos = info.getSubChecklists();
|
349
|
} else {
|
350
|
// otherwise we only look for the responses of one checklist
|
351
|
ServiceProviderInfos = new ArrayList<ServiceProviderInfo>(1);
|
352
|
ServiceProviderInfos.add(info);
|
353
|
}
|
354
|
|
355
|
// --- order the tnrResponses
|
356
|
for(ServiceProviderInfo subInfo : ServiceProviderInfos){
|
357
|
tnrResponse = null;
|
358
|
for(Response tnrr : tnrResponses){
|
359
|
// TODO compare by id, requires model change
|
360
|
if(subInfo.getLabel().equals(tnrr.getChecklist())){
|
361
|
tnrResponse = tnrr;
|
362
|
tnrStatus.setDuration(BigDecimal.valueOf(runner.getDuration()));
|
363
|
tnrResponsesOrderd.add(tnrResponse);
|
364
|
}
|
365
|
}
|
366
|
}
|
367
|
|
368
|
}
|
369
|
currentQuery.getClientStatus().add(tnrStatus);
|
370
|
}
|
371
|
currentQuery.getResponse().clear();
|
372
|
currentQuery.getResponse().addAll(tnrResponsesOrderd);
|
373
|
|
374
|
|
375
|
return tnrMsg;
|
376
|
}
|
377
|
|
378
|
|
379
|
}
|