Project

General

Profile

Download (16.4 KB) Statistics
| Branch: | Tag: | Revision:
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/bgbm/biovel/drf");
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
}
(2-2/2)