001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.ws.server.provider;
015
016import org.gbif.api.model.checklistbank.search.NameUsageSearchRequest.NameUsageQueryField;
017import org.gbif.api.model.common.search.SearchParameter;
018import org.gbif.api.model.common.search.SearchRequest;
019import org.gbif.api.model.common.search.SearchRequest.QueryField;
020import org.gbif.api.util.SearchTypeValidator;
021import org.gbif.api.util.VocabularyUtils;
022import org.gbif.ws.CommonRuntimeException;
023
024import java.lang.reflect.InvocationTargetException;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.Objects;
032import java.util.stream.Collectors;
033
034import org.apache.commons.lang3.StringUtils;
035import org.springframework.web.context.request.WebRequest;
036
037import static org.gbif.ws.util.CommonWsUtils.getFirst;
038import static org.gbif.ws.util.WebserviceParameter.PARAM_HIGHLIGHT;
039import static org.gbif.ws.util.WebserviceParameter.PARAM_QUERY_FIELD;
040import static org.gbif.ws.util.WebserviceParameter.PARAM_QUERY_STRING;
041import static org.gbif.ws.util.WebserviceParameter.PARAM_SPELLCHECK;
042import static org.gbif.ws.util.WebserviceParameter.PARAM_SPELLCHECK_COUNT;
043
044/**
045 * Provider class that transforms a set of HTTP parameters into a SearchRequest class instance.
046 * This assumes the existence of the following parameters in the HTTP request:
047 * 'page_size', 'offset', 'q' and any of the search parameter enum member names case insensitively.
048 */
049public class SearchRequestProvider<RT extends SearchRequest<P>, P extends Enum<?> & SearchParameter>
050    implements ContextProvider<RT> {
051
052  private static final int MAX_PAGE_SIZE = 1000;
053  private static final int NON_SPELL_CHECK_COUNT = -1;
054
055  private final Class<P> searchParameterClass;
056  private final Class<RT> requestType;
057  private final Integer maxPageSize;
058
059  public SearchRequestProvider(Class<RT> requestType, Class<P> searchParameterClass) {
060    this.requestType = requestType;
061    this.searchParameterClass = searchParameterClass;
062    this.maxPageSize = MAX_PAGE_SIZE;
063  }
064
065  public SearchRequestProvider(
066      Class<RT> requestType, Class<P> searchParameterClass, Integer maxPageSize) {
067    this.requestType = requestType;
068    this.searchParameterClass = searchParameterClass;
069    this.maxPageSize = maxPageSize;
070  }
071
072  @Override
073  public RT getValue(WebRequest webRequest) {
074    try {
075      RT req = requestType.getDeclaredConstructor().newInstance();
076      return getSearchRequest(webRequest, req);
077    } catch (InstantiationException
078        | IllegalAccessException
079        | NoSuchMethodException
080        | InvocationTargetException e) {
081      // should never happen
082      throw new CommonRuntimeException(e);
083    }
084  }
085
086  protected P findSearchParam(String name) {
087    try {
088      return VocabularyUtils.lookupEnum(name, searchParameterClass);
089    } catch (IllegalArgumentException e) {
090      // we have all params here, not only the enum ones, so this is ok to end up here a few times
091    }
092    return null;
093  }
094
095  protected RT getSearchRequest(WebRequest webRequest, RT searchRequest) {
096    searchRequest.copyPagingValues(PageableProvider.getPagingRequest(webRequest, maxPageSize));
097
098    final Map<String, String[]> params = webRequest.getParameterMap();
099
100    getSearchRequestFromQueryParams(searchRequest, params);
101
102    return searchRequest;
103  }
104
105  /**
106   * Override this method for populating specific search/suggest requests
107   */
108  protected void getSearchRequestFromQueryParams(
109      RT searchRequest, final Map<String, String[]> params) {
110    final String q = getFirst(params, PARAM_QUERY_STRING);
111    final String highlightValue = getFirst(params, PARAM_HIGHLIGHT);
112    final String spellCheck = getFirst(params, PARAM_SPELLCHECK);
113    final String spellCheckCount = getFirst(params, PARAM_SPELLCHECK_COUNT);
114
115    if (StringUtils.isNotEmpty(q)) {
116      searchRequest.setQ(q);
117    }
118
119    if (StringUtils.isNotEmpty(highlightValue)) {
120      searchRequest.setHighlight(Boolean.parseBoolean(highlightValue));
121    }
122
123    if (StringUtils.isNotEmpty(spellCheck)) {
124      searchRequest.setSpellCheck(Boolean.parseBoolean(spellCheck));
125    }
126
127    if (StringUtils.isNotEmpty(spellCheckCount)) {
128      searchRequest.setSpellCheckCount(Integer.parseInt(spellCheckCount));
129    } else {
130      searchRequest.setSpellCheckCount(NON_SPELL_CHECK_COUNT);
131    }
132
133    if (params.get(PARAM_QUERY_FIELD) != null) {
134      searchRequest.setQFields(
135          Arrays.stream(params.get(PARAM_QUERY_FIELD))
136              .map(SearchRequestProvider::parseQField)
137              .filter(Objects::nonNull)
138              .collect(Collectors.toSet()));
139    }
140
141    // find search parameter enum based filters
142    setSearchParams(searchRequest, params);
143  }
144
145  /**
146   * Removes all empty and null parameters from the list.
147   * Each value is trimmed(String.trim()) in order to remove all sizes of empty parameters.
148   */
149  private static List<String> removeEmptyParameters(List<String> parameters) {
150    List<String> cleanParameters = new ArrayList<>(parameters.size());
151    for (String param : parameters) {
152      String cleanParam = StringUtils.trimToEmpty(param);
153      if (!cleanParam.isEmpty()) {
154        cleanParameters.add(cleanParam);
155      }
156    }
157    return cleanParameters;
158  }
159
160  /**
161   * Iterates over the params map and adds to the search request the recognized parameters (i.e.: those that have a
162   * correspondent value in the P generic parameter).
163   * Empty (of all size) and null parameters are discarded.
164   */
165  private void setSearchParams(RT searchRequest, Map<String, String[]> params) {
166    for (Entry<String, String[]> entry : params.entrySet()) {
167      P p = findSearchParam(entry.getKey());
168      if (p != null) {
169        final List<String> list =
170            entry.getValue() != null ? Arrays.asList(entry.getValue()) : Collections.emptyList();
171        for (String val : removeEmptyParameters(list)) {
172          // validate value for certain types
173          SearchTypeValidator.validate(p, val);
174          searchRequest.addParameter(p, val);
175        }
176      }
177    }
178  }
179
180  private static QueryField parseQField(String qField) {
181    try {
182      return NameUsageQueryField.valueOf(qField);
183    } catch (Exception ex) {
184      // do nothing
185    }
186    return null;
187  }
188}