org.emonocot.portal.controller.SearchController.java Source code

Java tutorial

Introduction

Here is the source code for org.emonocot.portal.controller.SearchController.java

Source

/*
 * This is eMonocot, a global online biodiversity information resource.
 *
 * Copyright  20112015 The Board of Trustees of the Royal Botanic Gardens, Kew and The University of Oxford
 *
 * eMonocot is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * eMonocot is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * The complete text of the GNU Affero General Public License is in the source repository as the file
 * COPYING.  It is also available from <http://www.gnu.org/licenses/>.
 */
package org.emonocot.portal.controller;

import java.rmi.RemoteException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrServerException;
import org.emonocot.api.CommentService;
import org.emonocot.api.OrganisationService;
import org.emonocot.api.ResourceService;
import org.emonocot.api.SearchableObjectService;
import org.emonocot.api.TypeAndSpecimenService;
import org.emonocot.api.UserService;
import org.emonocot.api.autocomplete.Match;
import org.emonocot.model.SearchableObject;
import org.emonocot.model.TypeAndSpecimen;
import org.emonocot.pager.CellSet;
import org.emonocot.pager.Cube;
import org.emonocot.pager.Dimension;
import org.emonocot.pager.FacetName;
import org.emonocot.pager.Page;
import org.emonocot.portal.controller.form.NcbiDto;
import org.emonocot.portal.format.annotation.FacetRequestFormat;
import org.emonocot.portal.view.geojson.Feature;
import org.emonocot.portal.view.geojson.FeatureCollection;
import org.emonocot.portal.ws.ncbi.NcbiService;
import org.restdoc.api.GlobalHeader;
import org.restdoc.api.MethodDefinition;
import org.restdoc.api.ParamValidation;
import org.restdoc.api.ResponseDefinition;
import org.restdoc.api.RestDoc;
import org.restdoc.api.RestResource;
import org.restdoc.api.Schema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;

/**
 *
 * @author ben
 *
 */
@Controller
public class SearchController {

    private static Logger queryLog = LoggerFactory.getLogger("query");

    private static Logger logger = LoggerFactory.getLogger(SearchController.class);

    private SearchableObjectService searchableObjectService;

    private CommentService commentService;

    private OrganisationService organisationService;

    private ResourceService resourceService;

    private TypeAndSpecimenService typeAndSpecimenService;

    private UserService userService;

    private NcbiService ncbiService;

    private ObjectMapper objectMapper;

    @Autowired
    public void setSearchableObjectService(SearchableObjectService searchableObjectService) {
        this.searchableObjectService = searchableObjectService;
    }

    @Autowired
    public void setCommentService(CommentService commentService) {
        this.commentService = commentService;
    }

    @Autowired
    public void setOrganisationService(OrganisationService organisationService) {
        this.organisationService = organisationService;
    }

    @Autowired
    public void setResourceService(ResourceService resourceService) {
        this.resourceService = resourceService;
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Autowired
    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Autowired
    public void setTypeAndSpecimenService(TypeAndSpecimenService typeAndSpecimenService) {
        this.typeAndSpecimenService = typeAndSpecimenService;
    }

    @Autowired
    public void setNcbiService(NcbiService ncbiService) {
        this.ncbiService = ncbiService;
    }

    /**
     * @param query
     * @param start
     * @param limit
     * @param spatial
     * @param responseFacets
     * @param sort
     * @param selectedFacets
     * @return
     */
    private Page<? extends SearchableObject> runQuery(String query, Integer start, Integer limit, String spatial,
            String[] responseFacets, Map<String, String> facetPrefixes, String sort,
            Map<String, String> selectedFacets) throws SolrServerException {
        Page<? extends SearchableObject> result = searchableObjectService.search(query, spatial, limit, start,
                responseFacets, facetPrefixes, selectedFacets, sort, "taxon-with-image");
        queryLog.info("Query: \'{}\', start: {}, limit: {}," + "facet: [{}], {} results",
                new Object[] { query, start, limit, selectedFacets, result.getSize() });
        result.putParam("query", query);

        return result;
    }

    /**
     *
     * @param view
     *            Set the view name
     * @param className
     *            Set the class name
     * @return the default limit
     */

    private String setView(String view, String className) {
        if (view == null || view == "") {
            return null;
        } else if (view.equals("grid")) {
            if (className == null || className == "") {
                return null;
            } else if (className.equals("org.emonocot.model.Image")) {
                return "grid";
            } else {
                return null;
            }

        }
        return view;
    }

    /**
     *
     * @param query
     *            Set the query
     * @param limit
     *            Limit the number of returned results
     * @param start
     *            Set the offset
     * @param facets
     *            The facets to set
     * @param view
     *            Set the view
     * @param model
     *            Set the model
     *
     * @return a model and view
     */
    @RequestMapping(value = "/search", method = RequestMethod.GET, produces = { "text/html", "*/*" })
    public String search(@RequestParam(value = "query", required = false) String query,
            @RequestParam(value = "limit", required = false, defaultValue = "24") Integer limit,
            @RequestParam(value = "start", required = false, defaultValue = "0") Integer start,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
            @RequestParam(value = "sort", required = false) String sort,
            @RequestParam(value = "view", required = false) String view, Model model) throws SolrServerException {

        Map<String, String> selectedFacets = null;
        if (facets != null && !facets.isEmpty()) {
            selectedFacets = new HashMap<String, String>();
            for (FacetRequest facetRequest : facets) {
                selectedFacets.put(facetRequest.getFacet(), facetRequest.getSelected());
            }
            logger.debug(selectedFacets.size() + " facets have been selected from " + facets.size() + " available");
        } else {
            logger.debug("There were no facets available to select from");
        }
        if (selectedFacets != null && !selectedFacets.isEmpty()) {
            for (String key : selectedFacets.keySet()) {
                String escapeSpaceFacet = selectedFacets.get(key).replace(" ", "\\ ").replace("\\\\", "\\");
                selectedFacets.put(key, escapeSpaceFacet);

            }
        }
        // Decide which facets to return
        List<String> responseFacetList = new ArrayList<String>();
        Map<String, String> facetPrefixes = new HashMap<String, String>();
        responseFacetList.add("base.class_s");
        if (selectedFacets == null) {
            responseFacetList.add(FacetName.FAMILY.getSolrField());
        } else {
            int taxFacetIdx = 1; //Start from FacetName.FAMILY
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; taxFacetIdx++) {
                FacetName fn = FacetName.taxonomyFacets[taxFacetIdx];
                if (!responseFacetList.contains(fn.getSolrField())) {
                    responseFacetList.add(fn.getSolrField());
                }
                if (!selectedFacets.containsKey(fn.getSolrField())) {
                    break;
                }
            }
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; ++taxFacetIdx) {
                selectedFacets.remove(FacetName.taxonomyFacets[taxFacetIdx].getSolrField());
            }
        }
        responseFacetList.add("taxon.distribution_TDWG_0_ss");
        responseFacetList.add("taxon.measurement_or_fact_threatStatus_txt");
        responseFacetList.add("taxon.measurement_or_fact_Lifeform_txt");
        responseFacetList.add("taxon.measurement_or_fact_Habitat_txt");
        responseFacetList.add("taxon.taxon_rank_s");
        responseFacetList.add("taxon.taxonomic_status_s");
        responseFacetList.add("searchable.sources_ss");
        String className = null;
        if (selectedFacets == null) {
            logger.debug("No selected facets, setting default response facets");
        } else {
            if (selectedFacets.containsKey("base.class_s")) {
                className = selectedFacets.get("base.class_s");
            }
            if (selectedFacets.containsKey("taxon.distribution_TDWG_0_ss")) {
                logger.debug("Adding region facet");
                responseFacetList.add("taxon.distribution_TDWG_1_ss");
                facetPrefixes.put("taxon.distribution_TDWG_1_ss",
                        selectedFacets.get("taxon.distribution_TDWG_0_ss") + "_");
            } else {
                selectedFacets.remove("taxon.distribution_TDWG_1_ss");
            }
        }
        String[] responseFacets = new String[] {};
        responseFacets = responseFacetList.toArray(responseFacets);
        view = setView(view, className);
        //limit = setLimit(view, className);

        // Run the search
        Page<? extends SearchableObject> result = runQuery(query, start, limit, null, responseFacets, facetPrefixes,
                sort, selectedFacets);

        result.putParam("view", view);
        result.setSort(sort);
        model.addAttribute("result", result);
        return "search";
    }

    @RequestMapping(value = "/search", method = RequestMethod.GET, produces = "application/json")
    public ResponseEntity<Page> searchAPI(@RequestParam(value = "query", required = false) String query,
            @RequestParam(value = "limit", required = false, defaultValue = "24") Integer limit,
            @RequestParam(value = "start", required = false, defaultValue = "0") Integer start,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
            @RequestParam(value = "x1", required = false) Double x1,
            @RequestParam(value = "y1", required = false) Double y1,
            @RequestParam(value = "x2", required = false) Double x2,
            @RequestParam(value = "y2", required = false) Double y2,
            @RequestParam(value = "sort", required = false) String sort, Model model) throws SolrServerException {

        spatial(query, x1, y1, x2, y2, null, limit, start, facets, sort, null, model);
        return new ResponseEntity<Page>((Page) model.asMap().get("result"), HttpStatus.OK);
    }

    @RequestMapping(value = "/search", method = RequestMethod.GET, produces = "application/javascript")
    public ResponseEntity<JSONPObject> searchAPIJSONP(@RequestParam(value = "query", required = false) String query,
            @RequestParam(value = "limit", required = false, defaultValue = "24") Integer limit,
            @RequestParam(value = "start", required = false, defaultValue = "0") Integer start,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
            @RequestParam(value = "x1", required = false) Double x1,
            @RequestParam(value = "y1", required = false) Double y1,
            @RequestParam(value = "x2", required = false) Double x2,
            @RequestParam(value = "y2", required = false) Double y2,
            @RequestParam(value = "sort", required = false) String sort,
            @RequestParam(value = "callback", required = true) String callback, Model model)
            throws SolrServerException {

        spatial(query, x1, y1, x2, y2, null, limit, start, facets, sort, null, model);
        return new ResponseEntity<JSONPObject>(new JSONPObject(callback, (Page) model.asMap().get("result")),
                HttpStatus.OK);
    }

    /**
     *
     * @param query
     *            Set the query
     * @param limit
     *            Limit the number of returned results
     * @param start
     *            Set the offset
     * @param facets
     *            The facets to set
     * @param x1
     *            the first latitude
     * @param x2
     *            the second latitude
     * @param y1
     *            the first longitude
     * @param y2
     *            the second longitude
     * @param view
     *            Set the view
     * @param model
     *            Set the model
     *
     * @return a model and view
     */
    @RequestMapping(value = "/spatial", method = RequestMethod.GET, produces = { "text/html", "*/*" })
    public String spatial(@RequestParam(value = "query", required = false) String query,
            @RequestParam(value = "x1", required = false) Double x1,
            @RequestParam(value = "y1", required = false) Double y1,
            @RequestParam(value = "x2", required = false) Double x2,
            @RequestParam(value = "y2", required = false) Double y2,
            @RequestParam(value = "featureId", required = false) String featureId,
            @RequestParam(value = "limit", required = false, defaultValue = "24") Integer limit,
            @RequestParam(value = "start", required = false, defaultValue = "0") Integer start,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
            @RequestParam(value = "sort", required = false) String sort,
            @RequestParam(value = "view", required = false) String view, Model model) throws SolrServerException {
        String spatial = null;
        DecimalFormat decimalFormat = new DecimalFormat("###0.0");
        if (x1 != null && y1 != null && x2 != null && y2 != null
                && (x1 != 0.0 && y1 != 0.0 && x2 != 0.0 && x2 != 0.0 && y2 != 0.0)) {
            spatial = "{!join to=taxon.distribution_ss from=location.tdwg_code_s}geo:\"Intersects("
                    + decimalFormat.format(x1) + " " + decimalFormat.format(y1) + " " + decimalFormat.format(x2)
                    + " " + decimalFormat.format(y2) + ")\"";
        }

        Map<String, String> selectedFacets = null;
        if (facets != null && !facets.isEmpty()) {
            selectedFacets = new HashMap<String, String>();
            for (FacetRequest facetRequest : facets) {
                selectedFacets.put(facetRequest.getFacet(), facetRequest.getSelected());
            }
            logger.debug(selectedFacets.size() + " facets have been selected from " + facets.size() + " available");
        } else {
            logger.debug("There were no facets available to select from");
        }

        // Decide which facets to return
        List<String> responseFacetList = new ArrayList<String>();
        responseFacetList.add("base.class_s");
        if (selectedFacets == null) {
            responseFacetList.add(FacetName.FAMILY.getSolrField());
        } else {
            int taxFacetIdx = 1; //Start from FacetName.FAMILY
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; taxFacetIdx++) {
                FacetName fn = FacetName.taxonomyFacets[taxFacetIdx];
                if (!responseFacetList.contains(fn.getSolrField())) {
                    responseFacetList.add(fn.getSolrField());
                }
                if (!selectedFacets.containsKey(fn.getSolrField())) {
                    break;
                }
            }
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; ++taxFacetIdx) {
                selectedFacets.remove(FacetName.taxonomyFacets[taxFacetIdx].getSolrField());
            }
        }
        responseFacetList.add("taxon.measurement_or_fact_threatStatus_txt");
        responseFacetList.add("taxon.measurement_or_fact_Lifeform_txt");
        responseFacetList.add("taxon.measurement_or_fact_Habitat_txt");
        responseFacetList.add("taxon.taxon_rank_s");
        responseFacetList.add("taxon.taxonomic_status_s");
        responseFacetList.add("searchable.sources_ss");
        String className = null;
        if (selectedFacets == null) {
            logger.debug("No selected facets, setting default response facets");
        } else {
            if (selectedFacets.containsKey("base.class_s")) {
                className = selectedFacets.get("base.class_s");
            }
            if (selectedFacets.containsKey("taxon.distribution_TDWG_0_ss")) {
                logger.debug("Removing continent facet");
                responseFacetList.remove("taxon.distribution_TDWG_0_ss");
            }
        }
        String[] responseFacets = new String[] {};
        responseFacets = responseFacetList.toArray(responseFacets);
        view = setView(view, className);
        //limit = setLimit(view, className);

        // Run the search
        Page<? extends SearchableObject> result = runQuery(query, start, limit, spatial, responseFacets, null, sort,
                selectedFacets);

        if (spatial != null) {
            result.putParam("x1", x1);
            result.putParam("y1", y1);
            result.putParam("x2", x2);
            result.putParam("y2", y2);
        }
        if (!StringUtils.isEmpty(featureId)) {
            result.putParam("featureId", featureId);
        }
        result.putParam("view", view);
        result.setSort(sort);
        model.addAttribute("result", result);
        return "spatial";
    }

    @RequestMapping(value = "/visualise", method = RequestMethod.GET, produces = "text/html")
    public String visualise(Model uiModel, @RequestParam(value = "rows", required = false) String rows,
            @RequestParam(value = "firstRow", required = false, defaultValue = "0") Integer firstRow,
            @RequestParam(value = "maxRows", required = false, defaultValue = "10") Integer maxRows,
            @RequestParam(value = "cols", required = false) String cols,
            @RequestParam(value = "firstCol", required = false, defaultValue = "0") Integer firstCol,
            @RequestParam(value = "maxCols", required = false, defaultValue = "5") Integer maxCols,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets,
            @RequestParam(value = "view", required = false, defaultValue = "bar") String view) throws Exception {

        Map<String, String> selectedFacets = null;
        if (facets != null && !facets.isEmpty()) {
            selectedFacets = new HashMap<String, String>();
            for (FacetRequest facetRequest : facets) {
                selectedFacets.put(facetRequest.getFacet(), facetRequest.getSelected());
            }
        }

        List<String> facetList = new ArrayList<String>();
        if (selectedFacets == null) {
            facetList.add(FacetName.FAMILY.getSolrField());
        } else {
            int taxFacetIdx = 1; // Start from FacetName.FAMILY
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; taxFacetIdx++) {
                FacetName fn = FacetName.taxonomyFacets[taxFacetIdx];
                if (!facetList.contains(fn.getSolrField())) {
                    facetList.add(fn.getSolrField());
                }
                if (!selectedFacets.containsKey(fn.getSolrField())) {
                    break;
                }
            }
            for (; taxFacetIdx < FacetName.taxonomyFacets.length; ++taxFacetIdx) {
                selectedFacets.remove(FacetName.taxonomyFacets[taxFacetIdx].getSolrField());
            }
        }
        facetList.add("taxon.distribution_TDWG_0_ss");
        facetList.add("taxon.taxon_rank_s");
        facetList.add("taxon.taxonomic_status_s");
        facetList.add("searchable.sources_ss");
        facetList.add("taxon.measurement_or_fact_threatStatus_txt");
        facetList.add("taxon.measurement_or_fact_Lifeform_txt");
        facetList.add("taxon.measurement_or_fact_Habitat_txt");

        Cube cube = new Cube(selectedFacets);
        cube.setDefaultLevel("taxon.order_s");
        Dimension taxonomy = new Dimension("taxonomy");
        cube.addDimension(taxonomy);

        taxonomy.addLevel("taxon.order_s", false);
        taxonomy.addLevel("taxon.family_ss", false);
        taxonomy.addLevel("taxon.subfamily_ss", false);
        taxonomy.addLevel("taxon.tribe_ss", false);
        taxonomy.addLevel("taxon.subtribe_ss", false);
        taxonomy.addLevel("taxon.genus_ss", false);

        Dimension distribution = new Dimension("distribution");
        cube.addDimension(distribution);

        distribution.addLevel("taxon.distribution_TDWG_0_ss", true);
        distribution.addLevel("taxon.distribution_TDWG_1_ss", true);
        distribution.addLevel("taxon.distribution_TDWG_2_ss", true);

        Dimension taxonRank = new Dimension("taxonRank");
        cube.addDimension(taxonRank);
        taxonRank.addLevel("taxon.taxon_rank_s", false);

        Dimension taxonomicStatus = new Dimension("taxonomicStatus");
        cube.addDimension(taxonomicStatus);
        taxonomicStatus.addLevel("taxon.taxonomic_status_s", false);

        Dimension lifeForm = new Dimension("lifeForm");
        cube.addDimension(lifeForm);
        lifeForm.addLevel("taxon.measurement_or_fact_Lifeform_txt", false);

        Dimension habitat = new Dimension("habitat");
        cube.addDimension(habitat);
        habitat.addLevel("taxon.measurement_or_fact_Habitat_txt", false);

        Dimension conservationStatus = new Dimension("conservationStatus");
        cube.addDimension(conservationStatus);
        conservationStatus.addLevel("taxon.measurement_or_fact_threatStatus_txt", false);

        Dimension withDescriptions = new Dimension("hasDescriptions");
        cube.addDimension(withDescriptions);
        withDescriptions.addLevel("taxon.descriptions_not_empty_b", false);

        Dimension withImages = new Dimension("hasImages");
        cube.addDimension(withImages);
        withImages.addLevel("taxon.images_not_empty_b", false);

        CellSet cellSet = searchableObjectService.analyse(rows, cols, firstCol, maxCols, firstRow, maxRows,
                selectedFacets, facetList.toArray(new String[facetList.size()]), cube);

        uiModel.addAttribute("cellSet", cellSet);
        uiModel.addAttribute("view", view);
        return "visualise";
    }

    /**
     * @param term
     *            The term to search for
     * @return A list of terms to serialize
     */
    @RequestMapping(value = "/autocomplete", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Match> autocomplete(@RequestParam(required = true) String term)
            throws SolrServerException {
        return searchableObjectService.autocomplete(term, 10, null);
    }

    @RequestMapping(value = "/autocomplete/comment", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Match> autocompleteComments(@RequestParam(required = true) String term)
            throws SolrServerException {
        return commentService.autocomplete(term, 10, null);
    }

    @RequestMapping(value = "/autocomplete/user", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Match> autocompleteUsers(@RequestParam(required = true) String term)
            throws SolrServerException {
        return userService.autocomplete(term, 10, null);
    }

    @RequestMapping(value = "/autocomplete/organisation", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Match> autocompleteOrganisations(@RequestParam(required = true) String term)
            throws SolrServerException {
        return organisationService.autocomplete(term, 10, null);
    }

    @RequestMapping(value = "/autocomplete/resource", method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Match> autocompleteResources(@RequestParam(required = true) String term)
            throws SolrServerException {
        return resourceService.autocomplete(term, 10, null);
    }

    @ExceptionHandler(SolrServerException.class)
    @ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE)
    public ModelAndView handleObjectNotFoundException(SolrServerException sse) {
        ModelAndView modelAndView = new ModelAndView("serviceUnavailable");
        modelAndView.addObject("exception", sse);
        return modelAndView;
    }

    @RequestMapping(method = RequestMethod.OPTIONS, produces = "application/json")
    public ResponseEntity<RestDoc> optionsResource() throws JsonMappingException {
        RestDoc restDoc = new RestDoc();
        HashMap<String, Schema> schemas = new HashMap<String, Schema>();
        Schema pagerSchema = new Schema();
        SchemaFactoryWrapper pageVisitor = new SchemaFactoryWrapper();
        objectMapper.acceptJsonFormatVisitor(objectMapper.constructType(Page.class), pageVisitor);
        pagerSchema.setSchema(pageVisitor.finalSchema());
        schemas.put("http://e-monocot.org#page", pagerSchema);
        restDoc.setSchemas(schemas);

        GlobalHeader headers = new GlobalHeader();
        headers.request("Content-Type", "Must be set to application/json", true);
        headers.request("Authorization", "Supports HTTP Basic. Users may also use their api key", false);

        restDoc.setHeaders(headers);

        ParamValidation integerParam = new ParamValidation();
        integerParam.setType("match");
        integerParam.setPattern("\\d+");
        ParamValidation apikeyParam = new ParamValidation();
        apikeyParam.setType("match");
        apikeyParam.setPattern("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
        ParamValidation stringParam = new ParamValidation();
        stringParam.setType("match");
        stringParam.setPattern("[0-9a-f]+");
        ParamValidation doubleParam = new ParamValidation();
        doubleParam.setType("match");
        doubleParam.setPattern("[0-9]+\\.[0.9]+");

        Set<RestResource> resources = new HashSet<RestResource>();

        RestResource searchForObjects = new RestResource();
        searchForObjects.setId("Search");
        searchForObjects.setPath("/search{?query,x1,y1,x2,y2,facet,limit,start,sort,callback,apikey,fetch}");
        searchForObjects.param("limit", "The maximum number of resources to return", integerParam);
        searchForObjects.param("start",
                "The number of pages (of size _limit_) offset from the beginning of the recordset", integerParam);
        searchForObjects.param("apikey", "The apikey of the user account making the request", apikeyParam);
        searchForObjects.param("callback", "The name of the callback function used to wrap the JSON response",
                stringParam);
        searchForObjects.param("x1",
                "The southerly extent of the bounding box (uses WGS84 Coordinate reference system). Only documents with distributions within the bounding box will be returned",
                doubleParam);
        searchForObjects.param("y1",
                "The westerly extent of the bounding box (uses WGS84 Coordinate reference system). Only documents with distributions within the bounding box will be returned",
                doubleParam);
        searchForObjects.param("x2",
                "The northerly extent of the bounding box (uses WGS84 Coordinate reference system). Only documents with distributions within the bounding box will be returned",
                doubleParam);
        searchForObjects.param("y2",
                "The easterly extent of the bounding box (uses WGS84 Coordinate reference system). Only documents with distributions within the bounding box will be returned",
                doubleParam);
        searchForObjects.param("query",
                "A free-text query string. Only documents matching the query string will be returned", stringParam);
        searchForObjects.param("facet",
                "Only return documents which match a particular filter, in the form {fieldName}:{fieldValue} where fieldName is from the controlled vocabulary defined by org.emonocot.pager.FacetName.",
                stringParam);
        searchForObjects.param("sort",
                "Sort the result set according to the supplied criteria, in the form {fieldName}_(asc|desc) where fieldName is from the controlled vocabulary defined by org.emonocot.pager.FacetName.",
                stringParam);
        searchForObjects.param("fetch",
                "The name of a valid 'fetch-profile' which will load some or all related objects prior to serialization. Try 'object-page' to return most related objects",
                stringParam);

        MethodDefinition searchObjects = new MethodDefinition();
        searchObjects.description("Search for resources");
        ResponseDefinition searchObjectsResponseDefinition = new ResponseDefinition();

        searchObjectsResponseDefinition.type("application/json", "http://e-monocot.org#page");
        searchObjectsResponseDefinition.type("application/javascript", "http://e-monocot.org#page");
        searchObjects.response(searchObjectsResponseDefinition);
        searchObjects.statusCode("200", "Successfully searched for resources");

        searchForObjects.method("GET", searchObjects);
        resources.add(searchForObjects);

        restDoc.setResources(resources);

        return new ResponseEntity<RestDoc>(restDoc, HttpStatus.OK);
    }

    @RequestMapping(value = "/ncbi", method = RequestMethod.GET, consumes = "application/json", produces = "application/json")
    public ResponseEntity<NcbiDto> ncbi(@RequestParam(value = "query", required = true) String query) {
        NcbiDto ncbiDto = new NcbiDto();
        try {
            ncbiDto = ncbiService.issueRequest(query);
        } catch (RemoteException re) {
            logger.error("Exception using NCBI Service :" + re.getMessage(), re);
            return new ResponseEntity<NcbiDto>(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return new ResponseEntity<NcbiDto>(ncbiDto, HttpStatus.OK);
    }

    @RequestMapping(value = "/geo", method = RequestMethod.GET, consumes = "application/json", produces = "application/json")
    public ResponseEntity<FeatureCollection> spatial(@RequestParam(value = "query", required = false) String query,
            @RequestParam(value = "x1", required = false) Double x1,
            @RequestParam(value = "y1", required = false) Double y1,
            @RequestParam(value = "x2", required = false) Double x2,
            @RequestParam(value = "y2", required = false) Double y2,
            @RequestParam(value = "limit", required = false, defaultValue = "5000") Integer limit,
            @RequestParam(value = "start", required = false, defaultValue = "0") Integer start,
            @RequestParam(value = "facet", required = false) @FacetRequestFormat List<FacetRequest> facets)
            throws SolrServerException {
        String spatial = null;
        DecimalFormat decimalFormat = new DecimalFormat("###0.0");
        if (x1 != null && y1 != null && x2 != null && y2 != null
                && (x1 != 0.0 && y1 != 0.0 && x2 != 0.0 && x2 != 0.0 && y2 != 0.0)) {
            spatial = "geo:\"Intersects(" + decimalFormat.format(x1) + " " + decimalFormat.format(y1) + " "
                    + decimalFormat.format(x2) + " " + decimalFormat.format(y2) + ")\"";
        }

        Map<String, String> selectedFacets = new HashMap<String, String>();
        if (facets != null && !facets.isEmpty()) {
            for (FacetRequest facetRequest : facets) {
                selectedFacets.put(facetRequest.getFacet(), facetRequest.getSelected());
            }
            logger.debug(selectedFacets.size() + " facets have been selected from " + facets.size() + " available");
        } else {
            logger.debug("There were no facets available to select from");
        }
        selectedFacets.put("base.class_s", "org.emonocot.model.TypeAndSpecimen");

        // Run the search
        Page<TypeAndSpecimen> result = typeAndSpecimenQuery(query, start, limit, spatial, new String[] {}, null,
                null, selectedFacets);

        FeatureCollection featureCollection = new FeatureCollection();
        for (TypeAndSpecimen typeAndSpecimen : result.getRecords()) {
            featureCollection.getFeatures().add(Feature.fromTypeAndSpecimen(typeAndSpecimen));
        }
        return new ResponseEntity<FeatureCollection>(featureCollection, HttpStatus.OK);
    }

    private Page<TypeAndSpecimen> typeAndSpecimenQuery(String query, Integer start, Integer limit, String spatial,
            String[] responseFacets, Map<String, String> facetPrefixes, String sort,
            Map<String, String> selectedFacets) throws SolrServerException {
        Page<TypeAndSpecimen> result = typeAndSpecimenService.search(query, spatial, limit, start, responseFacets,
                facetPrefixes, selectedFacets, sort, null);
        result.putParam("query", query);

        return result;
    }
}