org.gbif.common.search.builder.SearchResponseBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.gbif.common.search.builder.SearchResponseBuilder.java

Source

/*
 * Copyright 2011 Global Biodiversity Information Facility (GBIF)
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gbif.common.search.builder;

import org.gbif.api.model.common.paging.Pageable;
import org.gbif.api.model.common.search.Facet;
import org.gbif.api.model.common.search.SearchParameter;
import org.gbif.api.model.common.search.SearchResponse;
import org.gbif.api.util.VocabularyUtils;
import org.gbif.api.vocabulary.Country;
import org.gbif.api.vocabulary.Language;
import org.gbif.common.search.exception.SearchException;
import org.gbif.common.search.model.HighlightableList;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

import com.google.common.collect.BiMap;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.gbif.common.search.util.AnnotationUtils.getKeyField;
import static org.gbif.common.search.util.AnnotationUtils.initFacetFieldsPropertiesMap;
import static org.gbif.common.search.util.AnnotationUtils.initFieldsPropertiesMap;
import static org.gbif.common.search.util.SolrConstants.HL_POST;
import static org.gbif.common.search.util.SolrConstants.HL_PRE;
import static org.gbif.common.search.util.SolrConstants.HL_PRE_REGEX;

/**
 * Builder class that helps in the creation process of {@link SearchResponse} objects from
 * {@link org.apache.solr.client.solrj.response.QueryResponse} objects.
 * The build method is not thread safe, supports the cloneable interface for allowing holds a partial state,
 * that can be cloned and repeated in other instances.
 * 
 * @param <T> is the response type.
 * @param <ST> is the type of the annotated class that holds the mapping information.
 * @param <P> is the search parameter enumeration.
 */
public class SearchResponseBuilder<T, ST extends T, P extends Enum<?> & SearchParameter> {

    // Logger
    private static final Logger LOG = LoggerFactory.getLogger(SearchResponseBuilder.class);

    // Holds a map containing the solr fields vs. application facet names
    private final BiMap<String, P> solrField2ParamEnumMap;

    // Holds a map containing the solr fields vs. java fields names
    private final BiMap<String, String> solrField2javaPropertiesMap;

    // Holds a map containing the highlightable solr fields vs. java fields names
    private Map<String, String> hlFieldPropertyPropertiesMap;

    // Name of the field annotated with the Key annotation.
    private final String keyField;

    // Class that contains the annotations for creating the response objects.
    private final Class<T> responseClass;
    private final Class<ST> annotatedClass;

    /**
     * Private full constructor of final fields only.
     */
    private SearchResponseBuilder(Class<T> responseClass, Class<ST> annotatedClass,
            BiMap<String, P> solrField2ParamEnumMap, BiMap<String, String> solrField2javaPropertiesMap) {
        this.responseClass = responseClass;
        this.annotatedClass = annotatedClass;
        this.solrField2ParamEnumMap = solrField2ParamEnumMap;
        this.solrField2javaPropertiesMap = solrField2javaPropertiesMap;
        this.keyField = getKeyField(annotatedClass);
    }

    /**
     * Private default constructor.
     */
    private SearchResponseBuilder(Class<T> responseClass, Class<ST> annotatedClass, Class<P> enumClass) {
        this.responseClass = responseClass;
        this.annotatedClass = annotatedClass;
        this.solrField2ParamEnumMap = initFacetFieldsPropertiesMap(annotatedClass, enumClass);
        this.solrField2javaPropertiesMap = initFieldsPropertiesMap(annotatedClass);
        this.keyField = getKeyField(annotatedClass);
    }

    /**
     * Factory method that reuses a facets mapping map and the default factory method.
     * 
     * @param annotatedClass that contains search related mappings and is the result the of the response
     *        content.
     * @param facetFieldsPropertiesMap map containing facet fields names <-> solr fields names.
     * @return a new instance of a SearchResponseBuilder
     */
    public static <T, ST extends T, P extends Enum<?> & SearchParameter> SearchResponseBuilder<T, ST, P> create(
            Class<T> responseClass, Class<ST> annotatedClass, BiMap<String, P> facetFieldsPropertiesMap,
            BiMap<String, String> fieldPropertyPropertiesMap) {

        return new SearchResponseBuilder<T, ST, P>(responseClass, annotatedClass, facetFieldsPropertiesMap,
                fieldPropertyPropertiesMap);
    }

    /**
     * Default factory method.
     * 
     * @param annotatedClass that contains search related mappings and is the result the of the response content.
     * @return a new instance of a SearchResponseBuilder
     */
    public static <T, ST extends T, P extends Enum<?> & SearchParameter> SearchResponseBuilder<T, ST, P> create(
            Class<T> responseClass, Class<ST> annotatedClass, Class<P> enumClass) {
        return new SearchResponseBuilder<T, ST, P>(responseClass, annotatedClass, enumClass);
    }

    /**
     * Builds a SearchResponse instance using the current builder state.
     * 
     * @return a new instance of a SearchResponse.
     */
    public SearchResponse<T, P> build(Pageable searchRequest, QueryResponse queryResponse) {
        // Create response
        SearchResponse<T, P> searchResponse = new SearchResponse<T, P>(searchRequest);
        searchResponse.setCount(queryResponse.getResults().getNumFound());
        searchResponse.setLimit(queryResponse.getResults().size());
        // The results and facets are copied into the response
        final List<ST> resultsST = queryResponse.getBeans(annotatedClass);
        // convert types
        final List<T> results = Lists.newArrayList();
        for (ST x : resultsST) {
            results.add(x);
        }
        searchResponse.setResults(results);
        searchResponse.setFacets(getFacetsFromResponse(queryResponse));
        setHighlightedFields(searchResponse, queryResponse);
        return searchResponse;
    }

    /**
     * Builds a simple SearchResponse instance using the current builder state, facets and highlighting reponses are
     * ommited.
     * 
     * @return a new instance of a SearchResponse.
     */
    public static <T, ST extends T, P extends SearchParameter> SearchResponse<T, P> buildSuggestReponse(
            Pageable searchRequest, QueryResponse queryResponse, Class<ST> annotatedClass) {
        // Create response
        SearchResponse<T, P> searchResponse = new SearchResponse<T, P>(searchRequest);
        searchResponse.setCount(queryResponse.getResults().getNumFound());
        searchResponse.setLimit(queryResponse.getResults().size());
        // The results and facets are copied into the response
        final List<ST> resultsST = queryResponse.getBeans(annotatedClass);
        // convert types
        final List<T> results = Lists.newArrayList();
        for (ST x : resultsST) {
            results.add(x);
        }
        searchResponse.setResults(results);
        return searchResponse;
    }

    /**
     * Copies the object's content into a new object.
     */
    public SearchResponseBuilder<T, ST, P> getCopy() {
        SearchResponseBuilder<T, ST, P> searchResponseBuilder = new SearchResponseBuilder<T, ST, P>(responseClass,
                annotatedClass, solrField2ParamEnumMap, solrField2javaPropertiesMap);
        searchResponseBuilder.hlFieldPropertyPropertiesMap = hlFieldPropertyPropertiesMap;
        return searchResponseBuilder;
    }

    /**
     * Sets the list of highlighted fields.
     */
    public SearchResponseBuilder<T, ST, P> withHighlightFields(List<String> highlightFields) {
        hlFieldPropertyPropertiesMap = new HashMap<String, String>();
        for (String hlField : highlightFields) {
            if (solrField2javaPropertiesMap.containsKey(hlField)) {
                hlFieldPropertyPropertiesMap.put(hlField, solrField2javaPropertiesMap.get(hlField));
            }
        }
        return this;
    }

    /**
     * Cleans all occurrences of highlighted tags/marks in the parameter and returns an new instance clean of those
     * marks.
     */
    private String cleanHighlightingMarks(final String hlText) {
        String hlLiteral = hlText;
        int indexPre = hlLiteral.indexOf(HL_PRE);
        while (indexPre > -1) {
            int indexPost = hlLiteral.indexOf(HL_POST, indexPre + HL_PRE.length());
            if (indexPost > -1) {
                String post = hlLiteral.substring(indexPost + HL_POST.length());
                String pre = hlLiteral.substring(0, indexPost);
                Matcher preMatcher = HL_PRE_REGEX.matcher(pre);
                pre = preMatcher.replaceFirst("");
                hlLiteral = pre + post;
            }
            indexPre = hlLiteral.indexOf(HL_PRE);
        }
        return hlLiteral;
    }

    private T getByKey(SearchResponse<T, P> response, String key) {
        for (T bean : response.getResults()) {
            try {
                String value = BeanUtils.getProperty(bean, keyField);
                if (value.equals(key)) {
                    return bean;
                }
            } catch (IllegalAccessException e) {
                LOG.error("Error accesing key field", e);
                throw new SearchException(e);
            } catch (InvocationTargetException e) {
                LOG.error("Error invoking set key field", e);
                throw new SearchException(e);
            } catch (NoSuchMethodException e) {
                LOG.error("Error invoking setter method", e);
                throw new SearchException(e);
            }
        }
        return null;
    }

    /**
     * Helper method that takes Solr response and extracts the facets results.
     * The facets are converted to a list of Facets understood by the search API.
     * The result of this method can be a empty list.
     * 
     * @param queryResponse that contains the facets information returned by Solr
     * @return the List of facets retrieved from the Solr response
     */
    private List<Facet<P>> getFacetsFromResponse(final QueryResponse queryResponse) {
        List<Facet<P>> facets = Lists.newArrayList();
        if (queryResponse.getFacetFields() != null) {
            for (final FacetField facetField : queryResponse.getFacetFields()) {
                P facetParam = solrField2ParamEnumMap.get(facetField.getName());
                Facet<P> facet = new Facet<P>(facetParam);

                List<Facet.Count> counts = Lists.newArrayList();
                if (facetField.getValues() != null) {
                    for (final FacetField.Count count : facetField.getValues()) {
                        String value = count.getName();
                        if (Enum.class.isAssignableFrom(facetParam.type())) {
                            value = getFacetEnumValue(facetParam, value);
                        }
                        counts.add(new Facet.Count(value, count.getCount()));
                    }
                }
                facet.setCounts(counts);
                facets.add(facet);
            }
        }
        return facets;
    }

    /**
     * Gets the facet value of Enum type parameter.
     * If the Enum is either a Country or a Language, its iso2Letter code it's used.
     */
    private String getFacetEnumValue(P facetParam, String value) {
        // the expected enum type for the value if it is an enum - otherwise null
        final Enum<?>[] enumValues = ((Class<? extends Enum<?>>) facetParam.type()).getEnumConstants();
        // if we find integers these are ordinals, translate back to enum names
        final Integer intValue = Ints.tryParse(value);
        if (intValue != null) {
            final Enum<?> enumValue = enumValues[intValue];
            if (Country.class.equals(facetParam.type())) {
                return ((Country) enumValue).getIso2LetterCode();
            } else if (Language.class.equals(facetParam.type())) {
                return ((Language) enumValue).getIso2LetterCode();
            } else {
                return enumValue.name();
            }
        } else {
            if (Country.class.equals(facetParam.type())) {
                return Country.fromIsoCode(value).getIso2LetterCode();
            } else if (Language.class.equals(facetParam.type())) {
                return Language.fromIsoCode(value).getIso2LetterCode();
            } else {
                return VocabularyUtils.lookupEnum(value, ((Class<? extends Enum<?>>) facetParam.type())).name();
            }
        }
    }

    /**
     * Takes the highlighted fields form solrResponse and copies them to the response object.
     * 
     * @param response to set the highlighted fields.
     * @param solrResponse to extract the highlighting information
     */
    private void setHighlightedFields(SearchResponse<T, P> response, final QueryResponse solrResponse) {
        if ((this.hlFieldPropertyPropertiesMap != null) && (solrResponse.getHighlighting() != null)
                && !solrResponse.getHighlighting().isEmpty()) {
            for (String docId : solrResponse.getHighlighting().keySet()) {
                setHighlightedFieldValues(response, solrResponse, docId);
            }
        }
    }

    /**
     * Sets the value of the highlighted field in object 'bean'.
     */
    private void setHighlightedFieldValue(T bean, String solrField, String hlValue) {
        try {
            if (List.class.isAssignableFrom(
                    PropertyUtils.getPropertyType(bean, solrField2javaPropertiesMap.get(solrField)))) {
                List<String> listProperty = (List<String>) PropertyUtils.getProperty(bean,
                        solrField2javaPropertiesMap.get(solrField));
                // Cleans the hl markers
                String hlCleanValue = cleanHighlightingMarks(hlValue);
                // Position of the value
                int hlValueIndex = listProperty.indexOf(hlCleanValue);
                if (hlValueIndex != -1) { // replace the value with the highlighted value
                    listProperty.remove(hlValueIndex);
                    listProperty.add(hlValueIndex, hlValue);
                }

            } else if (HighlightableList.class.isAssignableFrom(
                    PropertyUtils.getPropertyType(bean, solrField2javaPropertiesMap.get(solrField)))) {
                HighlightableList property = (HighlightableList) PropertyUtils.getProperty(bean,
                        solrField2javaPropertiesMap.get(solrField));
                // Cleans the hl markers
                String hlCleanValue = cleanHighlightingMarks(hlValue);
                // Position of the value
                List<String> values = property.getValues();
                int hlValueIndex = values.indexOf(hlCleanValue);
                if (hlValueIndex != -1) { // replace the value with the highlighted value
                    property.replaceValue(hlValueIndex, hlValue);
                }

            } else {
                String propertyName = solrField2javaPropertiesMap.get(solrField);
                if (PropertyUtils.getPropertyDescriptor(bean, propertyName).getPropertyType()
                        .isAssignableFrom(String.class)) {
                    BeanUtils.setProperty(bean, propertyName, hlValue);
                }
            }
        } catch (IllegalAccessException e) {
            LOG.error("Error accessing field for setting highlighted value", e);
            throw new SearchException(e);
        } catch (InvocationTargetException e) {
            LOG.error("Error setting highlighted value", e);
            throw new SearchException(e);
        } catch (NoSuchMethodException e) {
            LOG.error("Error invoking method to set highlighted value", e);
            throw new SearchException(e);
        }
    }

    /**
     * Sets the highlighted field values of beans in the response object.
     */
    private void setHighlightedFieldValues(SearchResponse<T, P> response, final QueryResponse solrResponse,
            String docId) {
        T bean = getByKey(response, docId);
        if (bean != null) {
            Map<String, List<String>> docHighlights = solrResponse.getHighlighting().get(docId);
            for (String solrField : hlFieldPropertyPropertiesMap.keySet()) {
                if (docHighlights.containsKey(solrField)) {
                    for (String hlValue : docHighlights.get(solrField)) { // get each snippet
                        setHighlightedFieldValue(bean, solrField, hlValue);
                    }
                }
            }
        }
    }

}