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}