/******************************************************************************* * Gisgraphy Project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA * * Copyright 2008 Gisgraphy project * David Masclet <davidmasclet@gisgraphy.com> * * *******************************************************************************/ package com.gisgraphy.fulltext; import static com.gisgraphy.helper.StringHelper.isEmptyString; import static com.gisgraphy.helper.StringHelper.isNotEmptyString; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.params.ModifiableSolrParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gisgraphy.addressparser.Address; import com.gisgraphy.domain.geoloc.entity.Adm; import com.gisgraphy.domain.geoloc.entity.City; import com.gisgraphy.domain.geoloc.entity.CitySubdivision; import com.gisgraphy.domain.geoloc.entity.GisFeature; import com.gisgraphy.domain.geoloc.entity.Street; import com.gisgraphy.domain.valueobject.Constants; import com.gisgraphy.domain.valueobject.Output.OutputStyle; import com.gisgraphy.fulltext.spell.SpellCheckerConfig; import com.gisgraphy.geocoding.GeocodingService; import com.gisgraphy.serializer.common.OutputFormat; /** * * usefullmethod to process fulltext query by solr * * @author <a href="mailto:david.masclet@gisgraphy.com">David Masclet</a> * */ public class FulltextQuerySolrHelper { protected static String ALL_ADM1_NAME_ALL_ADM2_NAME = " all_adm1_name^0.2 all_adm2_name^0.2 "; public static final Float MIN_SCORE = 15F; protected static final Logger logger = LoggerFactory.getLogger(GeocodingService.class); public static final int NUMBER_OF_STREET_TO_RETRIEVE = 40; public static final String FUZZY_FACTOR = "0.7"; public static final String BOOST_EXACT_WORD_FACTOR = ""; public static final String FEATUREID_PREFIX = FullTextFields.FEATUREID.getValue()+":"; public static final String OPENSTREETMAPID_PREFIX = FullTextFields.OPENSTREETMAP_ID.getValue()+":"; public static final int MAX_RADIUS_IN_METER = 100000; private static SmartStreetDetection smartStreetDetection = new SmartStreetDetection(); private static OutputStyleHelper outputStyleHelper = new OutputStyleHelper(); public static String MM_NOT_ALL_WORD_REQUIRED ="3<-1 4<3"; public static String MM_ALL_WORD_REQUIRED ="100%%"; protected static String NESTED_QUERY_TEMPLATE = "_query_:\"{!edismax qf='name^25 all_name^10 fully_qualified_name %s' pf='all_label' ps=0 tie='0.1' bq=' %s' mm='%s' bf='%s'}%s\""; protected static String NESTED_QUERY_NOT_ALL_WORDS_REQUIRED_TEMPLATE = NESTED_QUERY_TEMPLATE; protected static String CITY_BOOST_QUERY="placetype:city^800 placetype:adm^600"; protected static String STREET_BOOST_QUERY="placetype:street^150"; // we need to consider adm1name for andora and brooklin protected static final String NESTED_QUERY_NUMERIC_TEMPLATE = "_query_:\"{!edismax qf='zipcode^1.2 pf=name^1.1' bq='placetype:City^2 population^2' bf='pow(map(population,0,0,0.0001),0.3) pow(map(city_population,0,0,0.0000001),0.3)' }%s\""; protected static final String NESTED_QUERY_ID_TEMPLATE = "_query_:\"{!edismax qf='feature_id^1.1 '}%s\"";//openstreetmap_id^1.1 protected static final String NESTED_QUERY_OPENSTREETMAP_ID_TEMPLATE = "_query_:\"{!edismax qf='openstreetmap_id^1.1 '}%s\"";//openstreetmap_id^1.1 protected static final String FQ_COUNTRYCODE = FullTextFields.COUNTRYCODE.getValue()+":%s"; protected static final String FQ_PLACETYPE = FullTextFields.PLACETYPE.getValue()+":"; protected static final String FQ_LOCATION = "{!bbox "+Constants.SPATIAL_FIELD_PARAMETER+"="+GisFeature.LOCATION_COLUMN_NAME+"}"; //http://rechneronline.de/function-graphs/ protected static String BF_NEAREST = "recip(geodist(),0.01,3000,1)";//first number impact the nearest (the more, the nearest got importance), two other the farest. 2/3 =>the highest score protected static String BF_POPULATION="pow(map(population,0,0,0.009),0.7) pow(map(city_population,0,0,0.0001),0.20)"; //http://wiki.apache.org/solr/FunctionQuery#recip //{!geofilt sfield=store}&pt=45.15,-93.85&d=5 protected static final String GEOLOC_QUERY_TEMPLATE = "_query_:\"{!bbox " + Constants.SPATIAL_FIELD_PARAMETER + "=" + GisFeature.LOCATION_COLUMN_NAME + " " + Constants.POINT_PARAMETER + "=%f,%f " + Constants.DISTANCE_PARAMETER + "=%f}\""; /** * @return A Representation of all the needed parameters */ public static ModifiableSolrParams parameterize(FulltextQuery query) { getConfigInFile(); /*logger.error("all words : "+NESTED_QUERY_TEMPLATE); logger.error("not all words : "+NESTED_QUERY_TEMPLATE);*/ boolean spellchecker = true; ModifiableSolrParams parameters = new ModifiableSolrParams(); parameters.set(Constants.INDENT_PARAMETER, query.isOutputIndented() ? "on" : "off"); parameters.set(Constants.ECHOPARAMS_PARAMETER, "none"); //pagination parameters.set(Constants.START_PARAMETER, String .valueOf(query.getFirstPaginationIndex() - 1));// sub 1 because solr start at 0 parameters.set(Constants.ROWS_PARAMETER, String.valueOf(query.getPagination() .getMaxNumberOfResults())); //xslt? if (query.getOutputFormat() == OutputFormat.ATOM) { parameters.set(Constants.STYLESHEET_PARAMETER, Constants.ATOM_STYLESHEET); } else if (query.getOutputFormat() == OutputFormat.GEORSS) { parameters.set(Constants.STYLESHEET_PARAMETER, Constants.GEORSS_STYLESHEET); } //set outputformat if (query.isSuggest()){ parameters.set(Constants.OUTPUT_FORMAT_PARAMETER, OutputFormat.JSON .getParameterValue()); } else { parameters.set(Constants.OUTPUT_FORMAT_PARAMETER, query.getOutputFormat() .getParameterValue()); } //set field list /*if (query.isSuggest()){ // parameters.set(Constants.FL_PARAMETER,"");//we took the one by default } else*/ if (query.getOutputFormat() == OutputFormat.ATOM || query.getOutputFormat() == OutputFormat.GEORSS) { // force Medium style if ATOM or Geo RSS parameters.set(Constants.FL_PARAMETER,outputStyleHelper.getFulltextFieldList(OutputStyle.MEDIUM, query.getOutput().getLanguageCode())); } else { parameters.set(Constants.FL_PARAMETER, outputStyleHelper.getFulltextFieldList(query.getOutput())); } //radius / point filter query if (query.getPoint() != null) { parameters.set(Constants.SPATIAL_FIELD_PARAMETER, GisFeature.LOCATION_COLUMN_NAME); parameters.add(Constants.POINT_PARAMETER,query.getPoint().getY()+","+query.getPoint().getX()); if(query.getRadius() != 0){ double radius = query.getRadius(); if (query.getRadius()> MAX_RADIUS_IN_METER){ radius=MAX_RADIUS_IN_METER; } //we do a bounding box parameters.add(Constants.FQ_PARAMETER, FQ_LOCATION); parameters.add(Constants.DISTANCE_PARAMETER,radius/1000+""); } /*else if(query.getRadius() == 0){ parameters.add(Constants.DISTANCE_PARAMETER,MAX_RADIUS+""); } */ } if (query.getCountryCode()!=null && !"".equals(query.getCountryCode().trim())){ parameters.add(Constants.FQ_PARAMETER, String.format(FQ_COUNTRYCODE,query.getCountryCode().toUpperCase())); } if (query.getPlaceTypes() != null && containsOtherThingsThanNull(query.getPlaceTypes())) { StringBuffer sb = new StringBuffer(); sb.append("("); boolean firstAppend=false; for (int i=0;i< query.getPlaceTypes().length;i++){ if (query.getPlaceTypes()[i] != null){ if (firstAppend){ sb.append(" OR "); } sb.append(query.getPlaceTypes()[i].getSimpleName()); firstAppend=true; } } sb.append(")"); parameters.add(Constants.FQ_PARAMETER, FQ_PLACETYPE+sb.toString()); } boolean isNumericQuery = isNumericQuery(query.getQuery()); StringBuffer querybuffer ; if (query.getQuery().startsWith(FEATUREID_PREFIX)){ spellchecker=false; String id = query.getQuery().substring(FEATUREID_PREFIX.length()); String queryString = String.format(NESTED_QUERY_ID_TEMPLATE,id); parameters.set(Constants.QUERY_PARAMETER, queryString); parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); /*if (query.getPoint() != null ){ parameters.set(Constants.BF_PARAMETER, BF_NEAREST); }*/ } else if (query.getQuery().startsWith(OPENSTREETMAPID_PREFIX)){ spellchecker=false; String id = query.getQuery().substring(OPENSTREETMAPID_PREFIX.length()); String queryString = String.format(NESTED_QUERY_OPENSTREETMAP_ID_TEMPLATE,id); parameters.set(Constants.QUERY_PARAMETER, queryString); parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); } else if (query.isSuggest()){ List<String> streetTypes = smartStreetDetection.getStreetTypes(query.getQuery()); if (!isStreetQuery(query) && streetTypes.size()==1){//only if there is no pacetype=street // parameters.set(Constants.BQ_PARAMETER, STREET_BOOST_QUERY); parameters.add(Constants.FQ_PARAMETER, FullTextFields.PLACETYPE.getValue()+":("+Street.class.getSimpleName()+")"); } parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.suggest .toString()); parameters.set(Constants.QUERY_PARAMETER, query.getQuery()); if(query.getPoint()!=null){ parameters.set(Constants.BF_PARAMETER, BF_NEAREST); } } else if (isNumericQuery(query.getQuery())) { parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); String queryString = String.format(NESTED_QUERY_NUMERIC_TEMPLATE,query.getQuery()); parameters.set(Constants.QUERY_PARAMETER, queryString); } else { // we overide the query type /*parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.standard.toString()); parameters.set(Constants.QUERY_PARAMETER, query.getQuery());*/ String bqField=""; List<String> streetTypes = smartStreetDetection.getStreetTypes(query.getQuery()); if ((!isStreetQuery(query) && streetTypes.size()==1)){ bqField=STREET_BOOST_QUERY; } else if (query.getPlaceTypes()==null || isAdministrative(query.getPlaceTypes())){ bqField=CITY_BOOST_QUERY;//we force boost to city because it is not a 'Typed' query } String bfField = ""; if (query.getPoint() != null ) {//promote nearest if point is specified bfField = BF_NEAREST; } else { bfField=BF_POPULATION; } String queryString; if (query.isFuzzy()){ if (streetTypes!=null && streetTypes.size()==1){ queryString = buildFuzzyWords(query.getQuery(),streetTypes.get(0)); } else { queryString = buildFuzzyWords(query.getQuery(),null); } } else { queryString = query.getQuery(); } /*if (query.isExactName()){ querybuffer = new StringBuffer(String.format(EXACT_NAME_QUERY_TEMPLATE,"",boost,boostNearest,queryString)); } else {*/ String mm ; if (!query.isAllwordsRequired()){ mm= MM_NOT_ALL_WORD_REQUIRED; //is_in = isStreetQuery(query)?IS_IN_SENTENCE:""; //querybuffer = new StringBuffer(String.format(NESTED_QUERY_NOT_ALL_WORDS_REQUIRED_TEMPLATE,is_in,boost,MM_NOT_ALL_WORD_REQUIRED,boostNearest,queryString)); } else { //with all word required we don't search in is_in //is_in= ""; mm=MM_ALL_WORD_REQUIRED; } String is_in=""; if (isAdministrative(query.getPlaceTypes())){ is_in=ALL_ADM1_NAME_ALL_ADM2_NAME; } querybuffer = new StringBuffer(String.format(NESTED_QUERY_TEMPLATE,is_in,bqField,mm,bfField,queryString)); //} parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); String querySolr = querybuffer.toString(); logger.error("querysolr="+querySolr); String queryAsStr = querySolr; parameters.set(Constants.QUERY_PARAMETER, queryAsStr); } if (SpellCheckerConfig.enabled && query.hasSpellChecking() && !isNumericQuery && !query.isSuggest() && spellchecker){ parameters.set(Constants.SPELLCHECKER_ENABLED_PARAMETER,"true"); parameters.set(Constants.SPELLCHECKER_QUERY_PARAMETER, query.getQuery()); parameters.set(Constants.SPELLCHECKER_COLLATE_RESULTS_PARAMETER,SpellCheckerConfig.collateResults); parameters.set(Constants.SPELLCHECKER_NUMBER_OF_SUGGESTION_PARAMETER,SpellCheckerConfig.numberOfSuggestion); parameters.set(Constants.SPELLCHECKER_DICTIONARY_NAME_PARAMETER,SpellCheckerConfig.spellcheckerDictionaryName.toString()); } return parameters; } protected static boolean isAdministrative(Class[] array){ boolean containsAdministrative = false; if (array != null) { for (int i = 0; i < array.length; i++) { if (array[i] == Adm.class || array[i] == CitySubdivision.class || array[i] == City.class) { containsAdministrative = true; } else { //there is other class that are not Administrative return false; } } } return containsAdministrative; } private static void getConfigInFile() { try { //File fileDir = new File("/home/gisgraphy/workspace/gisgraphy/etc/solrtemplates.txt"); File fileDir = new File("/usr/local/gisgraphy/solrtemplates.txt"); BufferedReader in = new BufferedReader( new InputStreamReader( new FileInputStream(fileDir), "UTF8")); NESTED_QUERY_TEMPLATE = in.readLine(); CITY_BOOST_QUERY=in.readLine(); //ALL_ADM1_NAME_ALL_ADM2_NAME= in.readLine(); in.close(); } catch (UnsupportedEncodingException e) { System.out.println(e.getMessage()); } catch (IOException e) { System.out.println(e.getMessage()); } catch (Exception e) { System.out.println(e.getMessage()); } } private static boolean containsOtherThingsThanNull(Class[] array) { if (array != null) { for (int i = 0; i < array.length; i++) { if (array[i] != null) { return true; } } } return false; } private static boolean isNumericQuery(String queryString) { try { Integer.parseInt(queryString); return true; } catch (NumberFormatException e) { return false; } } protected static boolean isStreetQuery(FulltextQuery query) { if (query.getPlaceTypes() != null && containsOtherThingsThanNull(query.getPlaceTypes())) { for (int i = 0; i < query.getPlaceTypes().length; i++) { if (query.getPlaceTypes()[i] != null && query.getPlaceTypes()[i] == Street.class) { return true; } } } return false; } protected static boolean isStreetQueryOnly(FulltextQuery query) { if (query.getPlaceTypes() != null && query.getPlaceTypes().length == 1 && query.getPlaceTypes()[0] == Street.class) { return true; } return false; } /** * @return A query string for the specified parameter (starting with '?') * the name of the parameters are defined in {@link Constants} */ public static String toQueryString(FulltextQuery fulltextQuery) { return ClientUtils.toQueryString(parameterize(fulltextQuery), false); } /** * @return A query string for the specified parameter (starting with '?') * the name of the parameters are defined in {@link Constants} */ public static String toQueryString(Address address, boolean fuzzy) { ModifiableSolrParams addressQuery = buildAddressQuery(address, fuzzy); if (addressQuery == null){ return null; } return ClientUtils.toQueryString(addressQuery, false); } public static ModifiableSolrParams toRawQuery(String q) { ModifiableSolrParams parameters = new ModifiableSolrParams(); parameters.set(Constants.QUERY_PARAMETER, q); parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); parameters.add(Constants.FQ_PARAMETER, FQ_PLACETYPE+"(Street)"); return parameters; } /* protected static String buildFuzzyQuery(FulltextQuery fulltextQuery){ String query=fulltextQuery.getQuery(); StringBuffer sbq= new StringBuffer(); String fuzzyWords = buildFuzzyWords(query); sbq.append(FullTextFields.ALL_NAME.getValue()).append(":").append(fuzzyWords); //name if (!fulltextQuery.isAllwordsRequired()){ if (isStreetQuery(fulltextQuery)){ //sbq.append(FullTextFields.IS_IN.getValue()).append(":").append(fuzzyWords); sbq.append(FullTextFields.IS_IN_CITIES.getValue()).append(":").append(fuzzyWords); //sbq.append(FullTextFields.IS_IN_PLACE.getValue()).append(":").append(fuzzyWords); //sbq.append(FullTextFields.IS_IN_ADM.getValue()).append(":").append(fuzzyWords); } if (!isStreetQueryOnly(fulltextQuery)){ sbq.append(FullTextFields.ALL_ADM1_NAME.getValue()).append(":").append(fuzzyWords); sbq.append(FullTextFields.ALL_ADM2_NAME.getValue()).append(":").append(fuzzyWords); } } return sbq.toString(); }*/ public static String clean(String s){ if (s!=null){ s= s.replaceAll("\\s+"," ").trim(); s=s.replaceAll("[\\-\\s]$", ""); s=s.replaceAll("\\s+"," "); } return s; } public static ModifiableSolrParams buildAddressQuery(Address address, boolean fuzzy){ ModifiableSolrParams parameters = new ModifiableSolrParams(); StringBuffer sbq= new StringBuffer(); if (address.getCountryCode()!=null && address.getCountryCode().length()==2){ parameters.add(Constants.FQ_PARAMETER, String.format(FQ_COUNTRYCODE,address.getCountryCode().toUpperCase())); } //pagination parameters.set(Constants.START_PARAMETER, 0);// sub 1 because solr start at 0 parameters.set(Constants.ROWS_PARAMETER, NUMBER_OF_STREET_TO_RETRIEVE); if (address.getStreetName() != null) { String streetSentenceToSearch = address.getStreetName(); if (address.getStreetType()!=null){ streetSentenceToSearch = address.getStreetName()+ " "+address.getStreetType(); } if (fuzzy){ sbq.append(FullTextFields.ALL_NAME.getValue()).append(":").append(buildFuzzyWords(streetSentenceToSearch,null)).append(" "); sbq.append(FullTextFields.ALL_NAME.getValue()).append(":").append(buildExactWords(streetSentenceToSearch)).append(" "); } else { sbq.append(FullTextFields.ALL_NAME.getValue()).append(":(").append(clean(streetSentenceToSearch)).append(") "); } //Set placetype to street parameters.add(Constants.FQ_PARAMETER, FullTextFields.PLACETYPE.getValue()+":"+Street.class.getSimpleName()); } //set city String city=""; if (isNotEmptyString(address.getCity())) { city += " " + address.getCity(); } else if (isNotEmptyString(address.getPostTown())){ city += " " + address.getPostTown(); } if (!"".equals(city)){ //we got a city String field ; if (address.getStreetName() != null){ //it is a city for a streetname field = FullTextFields.IS_IN_CITIES.getValue(); } else { //it is only a city field = FullTextFields.ALL_NAME.getValue(); parameters.add(Constants.FQ_PARAMETER, FullTextFields.PLACETYPE.getValue()+":"+City.class.getSimpleName()); } if (fuzzy){ sbq.append(field).append(":").append((clean(city))).append(" "); sbq.append(FullTextFields.IS_IN.getValue()).append(":(").append(clean(city)).append(") "); } else { sbq.append(field).append(":(").append(clean(city)).append(") "); sbq.append(FullTextFields.IS_IN.getValue()).append(":(").append(clean(city)).append(") "); } } //set state String choiceState = ""; String dependentLocality = address.getDependentLocality(); String state = address.getState(); if (isEmptyString(state) && isNotEmptyString(dependentLocality)) { choiceState = " " + dependentLocality; } else if (isNotEmptyString(state) && isEmptyString(dependentLocality)) { choiceState = " " + state; } else if (isNotEmptyString(state) && isNotEmptyString(dependentLocality)) { choiceState = " " + state + " " + dependentLocality; } choiceState = clean(choiceState); if (!"".equals(choiceState)){ //we got a state if (address.getStreetName()!=null){ //it is an adm for a street if (fuzzy){ sbq.append(" ").append(FullTextFields.IS_IN_ADM.getValue()).append(":").append(buildFuzzyWords(choiceState,null)).append(" "); } else { sbq.append(" ").append(FullTextFields.IS_IN_ADM.getValue()).append(":(").append(choiceState).append(") "); } } if (address.getCity()!=null || address.getZipCode() !=null){ //adm for a city or zip if (fuzzy){ sbq.append(" ").append(FullTextFields.ALL_ADM1_NAME.getValue()).append(":").append(buildFuzzyWords(choiceState,null)).append(" "); } else { sbq.append(" ").append(FullTextFields.ALL_ADM1_NAME.getValue()).append(":(").append(choiceState).append(") "); } } else { //we got only a state if (fuzzy){ sbq.append(" ").append(FullTextFields.ALL_NAME.getValue()).append(":").append(buildFuzzyWords(choiceState,null)).append(" "); } else { sbq.append(" ").append(FullTextFields.ALL_NAME.getValue()).append(":").append(":(").append(choiceState).append(") "); } parameters.add(Constants.FQ_PARAMETER, FullTextFields.PLACETYPE.getValue()+":"+Adm.class.getSimpleName()); } } //if (address.getZipCode()!=null){ // String field =FullTextFields.ZIPCODE.getValue(); //if (address.getStreetName()!=null){ //field =FullTextFields.IS_IN_ZIP.getValue(); //} //if (address.getCity()==null){ //it has already been added by city step // parameters.add(Constants.FQ_PARAMETER, FullTextFields.PLACETYPE.getValue()+":"+City.class.getSimpleName()); //} // sbq.append(" ").append(field).append(":").append(clean(address.getZipCode())).append(" ");//no fuzzy in zipcode //} parameters.set(Constants.QUERY_PARAMETER, sbq.toString()); parameters.set(Constants.QT_PARAMETER, Constants.SolrQueryType.advanced .toString()); if ("".equals(sbq.toString())){ return null; } return parameters; } protected static String buildFuzzyWords(String query,String stopWord){ if (query ==null){ return ""; } String[] words = query.split("[,\\s\\-\\–\\一]");//not slash StringBuffer sb = new StringBuffer(""); for (int i = 0;i<words.length ;i++){ String word = words[i].trim(); if (words!=null && !"".equals(word)){ if (stopWord!=null){ if(word.equalsIgnoreCase(stopWord) || word.length()<=3 || StringUtils.isNumeric(word)){ sb.append(words[i]+BOOST_EXACT_WORD_FACTOR+" "); } else { //if there is street type, we put search exact and fuzzy sb.append(" ").append(words[i]).append("~"+FUZZY_FACTOR+" ").append(words[i]+BOOST_EXACT_WORD_FACTOR+" "); } } else { //if there is no street type only search in fuzzy sb.append(" ").append(words[i]).append("~"+FUZZY_FACTOR+" "); } } } sb.append(""); return sb.toString().trim(); } protected static String buildExactWords(String query){ if (query ==null){ return ""; } String[] words = query.split("[,\\s\\-\\–\\一]");//not slash StringBuffer sb = new StringBuffer(" ("); for (int i = 0;i<words.length ;i++){ String word = words[i].trim(); if (words!=null && !"".equals(word) && !StringUtils.isNumericSpace(word) ){ sb.append(" ").append(words[i]).append("^20 "); } } sb.append(") "); return sb.toString(); } }