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