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.io.IOException;
022import java.net.MalformedURLException;
023import java.net.URL;
024
025import lucee.commons.io.res.Resource;
026import lucee.commons.io.res.util.ResourceUtil;
027import lucee.commons.lang.StringUtil;
028import lucee.runtime.exp.ApplicationException;
029import lucee.runtime.exp.ExpressionException;
030import lucee.runtime.exp.PageException;
031import lucee.runtime.ext.tag.TagImpl;
032import lucee.runtime.op.Caster;
033import lucee.runtime.search.IndexResult;
034import lucee.runtime.search.SearchCollection;
035import lucee.runtime.search.SearchCollectionSupport;
036import lucee.runtime.search.SearchException;
037import lucee.runtime.search.SearchIndex;
038import lucee.runtime.search.lucene2.LuceneSearchCollection;
039import lucee.runtime.tag.util.DeprecatedUtil;
040import lucee.runtime.type.Struct;
041import lucee.runtime.type.StructImpl;
042import lucee.runtime.type.util.ListUtil;
043
044/**
045* Populates collections with indexed data.
046**/
047public final class Index extends TagImpl {
048    
049
050    private static final String[] EMPTY = new String[0];
051
052
053        public static String[] EXTENSIONS=new String[]{"htm","html","cfm","cfml","dbm","dbml"};
054    
055
056        /** Specifies the index action. */
057        private String action;
058
059        /** Specifies the URL path for files if type = "file" and type = "path". When the collection is 
060        **      searched with cfsearch, the pathname is automatically be prepended to filenames and returned as 
061        **      the url attribute. */
062        private String urlpath;
063
064        /** Specifies the type of entity being indexed. Default is CUSTOM. */
065        private short type=-1;
066
067        /** Title for collection;
068        ** Query column name for type and a valid query name;
069        ** Permits searching collections by title or displaying a separate title from the key */
070        private String title;
071
072        private String language;
073
074
075        /** Specifies the comma-separated list of file extensions that CFML uses to index files if 
076        **      type = "Path". Default is HTM, HTML, CFM, CFML, DBM, DBML.
077        **      An entry of "*." returns files with no extension */
078        private String[] extensions=EXTENSIONS;
079
080        /**  */
081        private String key;
082
083    /** A custom field you can use to store data during an indexing operation. Specify a query column 
084    **  name for type and a query name. */
085        private String custom1;
086        private long timeout=10000;
087
088    /** A custom field you can use to store data during an indexing operation. Usage is the same as 
089    **  for custom1. */
090    private String custom2;
091
092    /** A custom field you can use to store data during an indexing operation. Usage is the same as 
093    **  for custom1. */
094    private String custom3;
095
096    /** A custom field you can use to store data during an indexing operation. Usage is the same as 
097    **  for custom1. */
098    private String custom4;
099
100        /** Specifies the name of the query against which the collection is generated. */
101        private String query;
102
103        /** Specifies a collection name. If you are indexing an external collection external = "Yes", 
104        **      specify the collection name, including fully qualified path. */
105        private SearchCollection collection;
106
107        /** Yes or No. Yes specifies, if type = "Path", that directories below the path specified in 
108        **      key are included in the indexing operation. */
109        private boolean recurse;
110
111        /**  */
112        private String body;
113        private String name;
114        
115
116        private String[] category=EMPTY;
117        private String categoryTree="";
118        private String status;
119        private String prefix;
120
121
122        private boolean throwontimeout=false;
123
124        @Override
125        public void release()   {
126                super.release();
127                action=null;
128                urlpath=null;
129                type=-1;
130                title=null;
131                language=null;
132                extensions=EXTENSIONS;
133                key=null;
134                
135                custom1=null;
136                custom2=null;
137                custom3=null;
138                custom4=null;
139                
140                query=null;
141                collection=null;
142                recurse=false;
143                body=null;
144                name=null;
145                
146                category=EMPTY;
147                categoryTree="";
148                status=null;
149                prefix=null;
150                timeout=10000;
151                throwontimeout=false;
152        }
153
154
155        /** set the value action
156        *  Specifies the index action.
157        * @param action value to set
158        **/
159        public void setAction(String action)    {
160                this.action=action.toLowerCase().trim();
161        }
162
163        /** set the value urlpath
164        *  Specifies the URL path for files if type = "file" and type = "path". When the collection is 
165        *       searched with cfsearch, the pathname is automatically be prepended to filenames and returned as 
166        *       the url attribute.
167        * @param urlpath value to set
168        **/
169        public void setUrlpath(String urlpath)  {
170                if(StringUtil.isEmpty(urlpath))return;
171                this.urlpath=urlpath.toLowerCase().trim();
172        }
173
174        /** set the value type
175        *  Specifies the type of entity being indexed. Default is CUSTOM.
176        * @param type value to set
177         * @throws PageException
178        **/
179        public void setType(String type) throws PageException   {
180                if(type==null)return;
181            try {
182            this.type=SearchIndex.toType(type);
183        } 
184            catch (SearchException e) {
185            throw Caster.toPageException(e);
186        }
187        }
188        
189        /**
190         * @param timeout the timeout in seconds
191         * @throws ApplicationException 
192         */
193        public void setTimeout(double timeout) throws ApplicationException {
194                
195                this.timeout = (long)(timeout*1000D);
196                if(this.timeout<0)
197                        throw new ApplicationException("attribute timeout must contain a positive number");
198                if(timeout==0)timeout=1;
199        }
200        
201        /** set the value throwontimeout
202        *  Yes or No. Specifies how   timeout conditions are handled. If the value is Yes, an exception is 
203        *       generated to provide notification of the timeout. If the value is No, execution continues. Default is Yes.
204        * @param throwontimeout value to set
205        **/
206        public void setThrowontimeout(boolean throwontimeout) {
207                this.throwontimeout = throwontimeout;
208        }
209
210        
211        
212        public void setName(String name){
213                this.name=name;
214        }
215
216        /** set the value title
217        *  Title for collection;
218        * Query column name for type and a valid query name;
219        * Permits searching collections by title or displaying a separate title from the key
220        * @param title value to set
221        **/
222        public void setTitle(String title)      {
223                this.title=title;
224        }
225
226        /** set the value custom1
227        *  A custom field you can use to store data during an indexing operation. Specify a query column 
228        *       name for type and a query name.
229        * @param custom1 value to set
230        **/
231        public void setCustom1(String custom1)  {
232                this.custom1=custom1;
233        }
234
235        /** set the value language
236        * @param language value to set
237        **/
238        public void setLanguage(String language)        {
239                if(StringUtil.isEmpty(language)) return;
240                this.language=Collection.validateLanguage(language);
241        }
242
243        /** set the value external
244        * @param external value to set
245         * @throws ApplicationException
246        **/
247        public void setExternal(boolean external) {
248                DeprecatedUtil.tagAttribute(pageContext,"Index", "external");
249        }
250
251        /** set the value extensions
252        * @param extensions value to set
253         * @throws PageException
254        **/
255        public void setExtensions(String extensions) throws PageException       {
256                if(extensions==null) return;
257                this.extensions=ListUtil.toStringArrayTrim(ListUtil.listToArray(extensions,','));
258        }
259
260        /** set the value key
261        *  
262        * @param key value to set
263        **/
264        public void setKey(String key)  {
265                this.key=key;
266        }
267
268        /** set the value custom2
269        *  A custom field you can use to store data during an indexing operation. Usage is the same as 
270        *       for custom1.
271        * @param custom2 value to set
272        **/
273        public void setCustom2(String custom2)  {
274                this.custom2=custom2;
275        }
276    
277    /**
278     * @param custom3 The custom3 to set.
279     */
280    public void setCustom3(String custom3) {
281        this.custom3 = custom3;
282    }
283
284
285    /**
286     * @param custom4 The custom4 to set.
287     */
288    public void setCustom4(String custom4) {
289        this.custom4 = custom4;
290    }
291
292        /** set the value query
293        *  Specifies the name of the query against which the collection is generated.
294        * @param query value to set
295        **/
296        public void setQuery(String query)      {
297                this.query=query;
298        }
299
300        /** set the value collection
301        *  Specifies a collection name. If you are indexing an external collection external = "Yes", 
302        *       specify the collection name, including fully qualified path.
303        * @param collection value to set
304         * @throws PageException
305        **/
306        public void setCollection(String collection) throws PageException       {
307                try {
308                    this.collection=pageContext.getConfig().getSearchEngine().getCollectionByName(collection.toLowerCase().trim());
309            }
310                catch (SearchException e) {
311            throw Caster.toPageException(e);
312        }
313                  
314        }
315
316        /** set the value recurse
317        *  Yes or No. Yes specifies, if type = "Path", that directories below the path specified in 
318        *       key are included in the indexing operation.
319        * @param recurse value to set
320        **/
321        public void setRecurse(boolean recurse) {
322                this.recurse=recurse;
323        }
324
325        /** set the value body
326        *  
327        * @param body value to set
328        **/
329        public void setBody(String body)        {
330                this.body=body;
331        }
332        
333        /**
334         * @param category the category to set
335         * @throws ApplicationException 
336         */
337        public void setCategory(String listCategories)  {
338                if(listCategories==null) return;
339                this.category = ListUtil.trimItems(ListUtil.listToStringArray(listCategories, ','));
340        }
341
342
343        /**
344         * @param categoryTree the categoryTree to set
345         * @throws ApplicationException 
346         */
347        public void setCategorytree(String categoryTree) {
348                if(categoryTree==null) return;
349                categoryTree=categoryTree.replace('\\', '/').trim();
350                if(StringUtil.startsWith(categoryTree, '/'))categoryTree=categoryTree.substring(1);
351                if(!StringUtil.endsWith(categoryTree, '/') && categoryTree.length()>0)categoryTree+="/";
352                this.categoryTree = categoryTree;
353        }
354
355
356        /**
357         * @param prefix the prefix to set
358         */
359        public void setPrefix(String prefix) {
360                this.prefix = prefix;
361        }
362
363
364        /**
365         * @param status the status to set
366         * @throws ApplicationException 
367         */
368        public void setStatus(String status) {
369                this.status = status;
370        }
371
372
373        @Override
374        public int doStartTag() throws PageException    {
375            // SerialNumber sn = pageContext.getConfig().getSerialNumber();
376            //if(sn.getVersion()==SerialNumber.VERSION_COMMUNITY)
377            //    throw new SecurityException("no access to this functionality with the "+sn.getStringVersion()+" version of lucee");
378            
379            try {
380                    if(action.equals("purge")) doPurge();
381                    else if(action.equals("update")) doUpdate();
382                        else if(action.equals("delete")) doDelete();
383                        else if(action.equals("refresh")) doRefresh();
384                        else if(action.equals("list")) doList();
385
386                        else throw new ApplicationException("invalid action name [" + action + "]","valid action names are [list,update, delete, purge, refresh]");
387                } catch (Exception e) {
388                        throw Caster.toPageException(e);
389                } 
390                return SKIP_BODY;
391        }
392
393        
394        /**
395         * @throws PageException
396         * @throws SearchException
397         * @throws IOException
398     * 
399     */
400    private void doRefresh() throws PageException, SearchException, IOException {
401        doPurge();
402        doUpdate();
403    }
404    
405    private void doList() throws ApplicationException, PageException {
406                required("index",action,"name",name);
407                pageContext.setVariable(name,((SearchCollectionSupport)collection).getIndexesAsQuery());
408        }
409
410
411    /**
412     * delete a collection
413         * @throws PageException
414         * @throws SearchException
415     */
416    private void doDelete() throws PageException, SearchException {
417        required("index",action,"collection",collection);
418        if(type!=SearchIndex.TYPE_CUSTOM)required("index",action,"key",key);
419        
420        // no type defined
421        if(type==-1) {
422            if(query!=null) {
423                type=SearchIndex.TYPE_CUSTOM;
424            }
425            else {
426                Resource file=null;
427                try {
428                    file=ResourceUtil.toResourceExisting(pageContext,key);
429                    pageContext.getConfig().getSecurityManager().checkFileLocation(file);
430                } 
431                catch (ExpressionException e) {}
432
433                
434                if(file!=null && file.exists() && file.isFile()) type=SearchIndex.TYPE_FILE;
435                else if(file!=null && file.exists() && file.isDirectory()) type=SearchIndex.TYPE_PATH;
436                else {
437                    try {
438                        new URL(key);
439                        type=SearchIndex.TYPE_URL;
440                    } catch (MalformedURLException e) {}
441                }
442            }
443        }
444        
445        collection.deleteIndex(pageContext,key,type,query);   
446    }
447
448    /**
449     * purge a collection
450     * @throws PageException
451     * @throws SearchException
452     */
453    private void doPurge() throws PageException, SearchException {
454        required("index",action,"collection",collection);
455        collection.purge();
456    }
457
458    /**
459     * update a collection
460     * @throws PageException
461     * @throws SearchException
462     * @throws IOException
463     */
464    private void doUpdate() throws PageException, SearchException, IOException {
465        // check attributes
466        required("index",action,"collection",collection);
467        required("index",action,"key",key);
468        
469        if(type==-1) type=(query==null)?SearchIndex.TYPE_FILE:SearchIndex.TYPE_CUSTOM;
470        
471        if(type==SearchIndex.TYPE_CUSTOM) {
472            required("index",action,"body",body);
473            //required("index",action,"query",query);
474        }
475        IndexResult result;
476        
477        // FUTURE remove this condition
478        if(collection instanceof LuceneSearchCollection)
479                result = ((LuceneSearchCollection)collection).index(pageContext,key,type,urlpath,title,body,language,extensions,query,recurse,categoryTree,category,timeout,custom1,custom2,custom3,custom4);
480        else
481                result = collection.index(pageContext,key,type,urlpath,title,body,language,extensions,query,recurse,categoryTree,category,custom1,custom2,custom3,custom4);
482         if(!StringUtil.isEmpty(status))pageContext.setVariable(status,toStruct(result));
483    }
484
485
486        @Override
487        public int doEndTag()   {
488                return EVAL_PAGE;
489        }
490
491        private Struct toStruct(IndexResult result) {
492                Struct sct=new StructImpl();
493                sct.setEL("deleted",new Double(result.getCountDeleted()));
494                sct.setEL("inserted",new Double(result.getCountInserted()));
495                sct.setEL("updated",new Double(result.getCountUpdated()));
496                return sct;
497        }
498
499}