001/** 002 * 003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved. 004 * 005 * This library is free software; you can redistribute it and/or 006 * modify it under the terms of the GNU Lesser General Public 007 * License as published by the Free Software Foundation; either 008 * version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 * Lesser General Public License for more details. 014 * 015 * You should have received a copy of the GNU Lesser General Public 016 * License along with this library. If not, see <http://www.gnu.org/licenses/>. 017 * 018 **/ 019package lucee.runtime.tag; 020 021import java.util.Iterator; 022import java.util.Map; 023 024import lucee.commons.lang.StringUtil; 025import lucee.runtime.exp.ApplicationException; 026import lucee.runtime.exp.PageException; 027import lucee.runtime.ext.tag.TagImpl; 028import lucee.runtime.op.Caster; 029import lucee.runtime.op.Decision; 030import lucee.runtime.search.AddionalAttrs; 031import lucee.runtime.search.SearchCollection; 032import lucee.runtime.search.SearchData; 033import lucee.runtime.search.SearchDataImpl; 034import lucee.runtime.search.SearchEngine; 035import lucee.runtime.search.SearchException; 036import lucee.runtime.search.SuggestionItem; 037import lucee.runtime.tag.util.DeprecatedUtil; 038import lucee.runtime.type.KeyImpl; 039import lucee.runtime.type.QueryImpl; 040import lucee.runtime.type.Struct; 041import lucee.runtime.type.StructImpl; 042import lucee.runtime.type.util.KeyConstants; 043import lucee.runtime.type.util.ListUtil; 044 045public final class Search extends TagImpl { 046 047 private static final String[] EMPTY = new String[0]; 048 049 private static final int SUGGESTIONS_ALWAYS = Integer.MAX_VALUE; 050 private static final int SUGGESTIONS_NEVER = -1; 051 052 private static final lucee.runtime.type.Collection.Key FOUND = KeyImpl.intern("found"); 053 private static final lucee.runtime.type.Collection.Key SEARCHED = KeyImpl.intern("searched"); 054 private static final lucee.runtime.type.Collection.Key KEYWORDS = KeyImpl.intern("keywords"); 055 private static final lucee.runtime.type.Collection.Key KEYWORD_SCORE = KeyImpl.intern("keywordScore"); 056 057 /** Specifies the criteria type for the search. */ 058 private short type=SearchCollection.SEARCH_TYPE_SIMPLE; 059 060 /** Specifies the maximum number of entries for index queries. If omitted, all rows are returned. */ 061 private int maxrows=-1; 062 063 /** Specifies the criteria for the search following the syntactic rules specified by type. */ 064 private String criteria=""; 065 066 /** Specifies the first row number to be retrieved. Default is 1. */ 067 private int startrow=1; 068 069 /** The logical collection name that is the target of the search operation or an external collection 070 ** with fully qualified path. */ 071 private SearchCollection[] collections; 072 073 /** A name for the search query. */ 074 private String name; 075 076 private String[] category=EMPTY; 077 private String categoryTree=""; 078 private String status; 079 private int suggestions=SUGGESTIONS_NEVER; 080 private int contextPassages=0; 081 private int contextBytes=300; 082 private String contextHighlightBegin="<b>"; 083 private String contextHighlightEnd="</b>"; 084 private String previousCriteria; 085 086 //private int spellCheckMaxLevel=10; 087 //private String result=null; 088 089 @Override 090 public void release() { 091 super.release(); 092 type=SearchCollection.SEARCH_TYPE_SIMPLE; 093 maxrows=-1; 094 criteria=""; 095 startrow=1; 096 collections=null; 097 098 category=EMPTY; 099 categoryTree=""; 100 status=null; 101 suggestions=SUGGESTIONS_NEVER; 102 contextPassages=0; 103 contextBytes=300; 104 contextHighlightBegin="<b>"; 105 contextHighlightEnd="</b>"; 106 previousCriteria=null; 107 108 109 //spellCheckMaxLevel=10; 110 //result=null; 111 112 } 113 114 /** set the value type 115 * Specifies the criteria type for the search. 116 * @param type value to set 117 * @throws ApplicationException 118 **/ 119 public void setType(String type) throws ApplicationException { 120 if(type==null) return; 121 type=type.toLowerCase().trim(); 122 if(type.equals("simple"))this.type=SearchCollection.SEARCH_TYPE_SIMPLE; 123 else if(type.equals("explicit"))this.type=SearchCollection.SEARCH_TYPE_EXPLICIT; 124 else 125 throw new ApplicationException("attribute type of tag search has an invalid value, valid values are [simple,explicit] now is ["+type+"]"); 126 127 } 128 129 /** set the value maxrows 130 * Specifies the maximum number of entries for index queries. If omitted, all rows are returned. 131 * @param maxrows value to set 132 **/ 133 public void setMaxrows(double maxrows) { 134 this.maxrows=(int) maxrows; 135 } 136 137 /** set the value criteria 138 * Specifies the criteria for the search following the syntactic rules specified by type. 139 * @param criteria value to set 140 **/ 141 public void setCriteria(String criteria) { 142 this.criteria=criteria; 143 } 144 145 /** set the value startrow 146 * Specifies the first row number to be retrieved. Default is 1. 147 * @param startrow value to set 148 **/ 149 public void setStartrow(double startrow) { 150 this.startrow=(int) startrow; 151 } 152 153 /** set the value collection 154 * The logical collection name that is the target of the search operation or an external collection 155 * with fully qualified path. 156 * @param collection value to set 157 * @throws PageException 158 **/ 159 public void setCollection(String collection) throws PageException { 160 String[] collNames=ListUtil.toStringArrayTrim(ListUtil.listToArrayRemoveEmpty(collection,',')); 161 collections=new SearchCollection[collNames.length]; 162 SearchEngine se = pageContext.getConfig().getSearchEngine(); 163 try { 164 for(int i=0;i<collections.length;i++) { 165 collections[i]=se.getCollectionByName(collNames[i]); 166 } 167 } catch (SearchException e) { 168 collections=null; 169 throw Caster.toPageException(e); 170 } 171 } 172 173 /** set the value language 174 * @param language value to set 175 **/ 176 public void setLanguage(String language) { 177 DeprecatedUtil.tagAttribute(pageContext,"Search", "language"); 178 } 179 180 /** set the value external 181 * @param external value to set 182 * @throws ApplicationException 183 **/ 184 public void setExternal(boolean external) throws ApplicationException { 185 DeprecatedUtil.tagAttribute(pageContext,"Search", "external"); 186 } 187 188 /** set the value name 189 * A name for the search query. 190 * @param name value to set 191 **/ 192 public void setName(String name) { 193 this.name=name; 194 } 195 196 197 198 /** 199 * @param category the category to set 200 * @throws ApplicationException 201 */ 202 public void setCategory(String listCategories) { 203 if(StringUtil.isEmpty(listCategories)) return; 204 this.category = ListUtil.trimItems(ListUtil.listToStringArray(listCategories, ',')); 205 } 206 207 208 /** 209 * @param categoryTree the categoryTree to set 210 * @throws ApplicationException 211 */ 212 public void setCategorytree(String categoryTree) { 213 if(StringUtil.isEmpty(categoryTree)) return; 214 categoryTree=categoryTree.replace('\\', '/').trim(); 215 if(StringUtil.startsWith(categoryTree, '/'))categoryTree=categoryTree.substring(1); 216 if(!StringUtil.endsWith(categoryTree, '/') && categoryTree.length()>0)categoryTree+="/"; 217 this.categoryTree = categoryTree; 218 } 219 220 /** 221 * @param contextBytes the contextBytes to set 222 * @throws ApplicationException 223 */ 224 public void setContextbytes(double contextBytes) throws ApplicationException { 225 this.contextBytes = (int)contextBytes; 226 } 227 228 /** 229 * @param contextHighlightBegin the contextHighlightBegin to set 230 * @throws ApplicationException 231 */ 232 public void setContexthighlightbegin(String contextHighlightBegin) { 233 this.contextHighlightBegin = contextHighlightBegin; 234 } 235 236 /** 237 * @param contextHighlightEnd the contextHighlightEnd to set 238 * @throws ApplicationException 239 */ 240 public void setContexthighlightend(String contextHighlightEnd) { 241 this.contextHighlightEnd = contextHighlightEnd; 242 } 243 244 /** 245 * @param contextPassages the contextPassages to set 246 * @throws ApplicationException 247 */ 248 public void setContextpassages(double contextPassages) throws ApplicationException { 249 this.contextPassages = (int)contextPassages; 250 } 251 252 253 254 255 /** 256 * @param previousCriteria the previousCriteria to set 257 * @throws ApplicationException 258 */ 259 public void setPreviouscriteria(String previousCriteria) throws ApplicationException { 260 this.previousCriteria = previousCriteria; 261 throw new ApplicationException("attribute previousCriteria for tag search is not supported yet"); 262 // TODO impl tag attribute 263 } 264 265 /** 266 * @param status the status to set 267 * @throws ApplicationException 268 */ 269 public void setStatus(String status) { 270 if(!StringUtil.isEmpty(status))this.status = status; 271 } 272 273 /** 274 * @param suggestions the suggestions to set 275 * @throws ApplicationException 276 */ 277 public void setSuggestions(String suggestions) throws PageException { 278 if(StringUtil.isEmpty(suggestions))return; 279 suggestions = suggestions.trim().toLowerCase(); 280 if("always".equals(suggestions)) this.suggestions=SUGGESTIONS_ALWAYS; 281 else if("never".equals(suggestions)) this.suggestions=SUGGESTIONS_NEVER; 282 else if(Decision.isNumeric(suggestions)) { 283 this.suggestions=Caster.toIntValue(suggestions); 284 } 285 else 286 throw new ApplicationException("attribute suggestions has an invalid value ["+suggestions+"], valid values are [always,never,<positive numeric value>]"); 287 288 289 } 290 291 @Override 292 public int doStartTag() throws PageException { 293 //SerialNumber sn = pageContext.getConfig().getSerialNumber(); 294 //if(sn.getVersion()==SerialNumber.VERSION_COMMUNITY) 295 // throw new SecurityException("no access to this functionality with the "+sn.getStringVersion()+" version of lucee"); 296 final String v="VARCHAR",d="DOUBLE"; 297 String[] cols = new String[]{"title","url","summary","score","recordssearched","key","custom1","custom2","custom3","custom4", 298 "categoryTree","category","context","size","rank","author","type","collection"}; 299 300 // TODO support context 301 String[] types = new String[]{v,v,v,d,d,v,v,v,v,v,v,v,v,d,d,v,v,v}; 302 SearchData data=new SearchDataImpl(suggestions); 303 SuggestionItem item=null;// this is already here to make sure the classloader load this sinstance 304 305 306 lucee.runtime.type.Query qry=new QueryImpl(cols,types,0,"query"); 307 308 SearchCollection collection; 309 long time=System.currentTimeMillis(); 310 AddionalAttrs.setAddionalAttrs(contextBytes,contextPassages,contextHighlightBegin,contextHighlightEnd); 311 try { 312 for(int i=0;i<collections.length;i++) { 313 collection=collections[i]; 314 startrow=collection.search(data,qry,criteria,collection.getLanguage(),type,startrow,maxrows,categoryTree,category); 315 316 if(maxrows>=0 && qry.getRecordcount()>=maxrows) break; 317 } 318 pageContext.setVariable(name,qry); 319 } 320 catch(SearchException se) { 321 throw Caster.toPageException(se); 322 } 323 finally{ 324 AddionalAttrs.removeAddionalAttrs(); 325 } 326 327 time=System.currentTimeMillis()-time; 328 Double recSearched=new Double(data.getRecordsSearched()); 329 int len=qry.getRecordcount(); 330 for(int i=1;i<=len;i++) { 331 qry.setAt("recordssearched",i,recSearched); 332 } 333 334 // status 335 if(status!=null) { 336 Struct sct=new StructImpl(); 337 pageContext.setVariable(status, sct); 338 sct.set(FOUND, new Double(qry.getRecordcount())); 339 sct.set(SEARCHED, recSearched); 340 sct.set(KeyConstants._time, new Double(time)); 341 342 // TODO impl this values 343 344 Map s = data.getSuggestion(); 345 if(s.size()>0) { 346 String key; 347 348 Iterator it = s.keySet().iterator(); 349 Struct keywords=new StructImpl(); 350 Struct keywordScore=new StructImpl(); 351 sct.set(KEYWORDS, keywords); 352 sct.set(KEYWORD_SCORE, keywordScore); 353 Object obj; 354 355 while(it.hasNext()) { 356 key=(String) it.next(); 357 // FUTURE add SuggestionItem as interface to public interface 358 359 // the problem is a conflict between the SuggestionItem version from core and extension 360 obj=s.get(key); 361 if(obj instanceof SuggestionItem){ 362 item=(SuggestionItem)obj; 363 keywords.set(key, item.getKeywords()); 364 keywordScore.set(key, item.getKeywordScore()); 365 } 366 else { 367 Class clazz = obj.getClass(); 368 try { 369 keywords.set(key, clazz.getMethod("getKeywords", new Class[0]).invoke(obj, new Object[0])); 370 keywordScore.set(key, clazz.getMethod("getKeywordScore", new Class[0]).invoke(obj, new Object[0])); 371 } 372 catch (Exception e) {} 373 } 374 375 376 377 } 378 379 380 String query = data.getSuggestionQuery(); 381 if(query!=null) { 382 String html = StringUtil.replace(query, "<suggestion>", "<b>", false); 383 html = StringUtil.replace(html, "</suggestion>", "</b>", false); 384 sct.set("suggestedQueryHTML", html); 385 386 String plain = StringUtil.replace(query, "<suggestion>", "", false); 387 plain = StringUtil.replace(plain, "</suggestion>", "", false); 388 sct.set("suggestedQuery", plain); 389 } 390 391 392 } 393 394 //if(suggestions!=SUGGESTIONS_NEVER)sct.set("suggestedQuery", ""); 395 //sct.set("keywords", ""); 396 //sct.set("keywordScore", ""); 397 398 399 } 400 401 402 return SKIP_BODY; 403 } 404 405 @Override 406 public int doEndTag() { 407 return EVAL_PAGE; 408 } 409 410 411}