Project

General

Profile

Download (16.3 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.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
}
(2-2/2)