001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.tagcloud.generators;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Comparator;
022import java.util.Iterator;
023import java.util.List;
024
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.ProcessingException;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.generation.ServiceableGenerator;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.solr.client.solrj.SolrClient;
038import org.apache.solr.client.solrj.SolrQuery;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.search.query.ContentTypeQuery;
042import org.ametys.cms.search.query.OrQuery;
043import org.ametys.cms.search.query.Query;
044import org.ametys.cms.search.query.QuerySyntaxException;
045import org.ametys.cms.search.solr.SolrClientProvider;
046import org.ametys.plugins.repository.AmetysObjectResolver;
047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
048import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
049import org.ametys.plugins.tagcloud.cache.TagCloudCacheManager;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.web.WebConstants;
052import org.ametys.web.indexing.solr.SolrWebFieldNames;
053import org.ametys.web.repository.page.Page;
054import org.ametys.web.repository.page.ZoneItem;
055import org.ametys.web.search.query.PageContentQuery;
056import org.ametys.web.search.query.PageQuery;
057
058/**
059 * Generator for tag clouds
060 */
061public abstract class AbstractTagCloudGenerator extends ServiceableGenerator implements Contextualizable
062{
063    
064    /** Compares tag cloud items  */
065    protected static final Comparator<TagCloudItem> OCCURRENCE_COMPARATOR = new ItemOccurrenceComparator();
066    
067    /** The Ametys object resolver */
068    protected AmetysObjectResolver _resolver;
069    /** The cache manager */
070    protected TagCloudCacheManager _cacheManager;
071    
072    /** The solr client provider. */
073    protected SolrClientProvider _solrClientProvider;
074    
075    /** The solr client. */
076    protected SolrClient _solrClient;
077    
078    /** The context */
079    protected Context _context;
080    
081    @Override
082    public void contextualize(Context context) throws ContextException
083    {
084        _context = context;
085    }
086    
087    @Override
088    public void service(ServiceManager serviceManager) throws ServiceException
089    {
090        super.service(serviceManager);
091        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
092        _cacheManager = (TagCloudCacheManager) serviceManager.lookup(TagCloudCacheManager.ROLE);
093        _solrClientProvider = (SolrClientProvider) serviceManager.lookup(SolrClientProvider.ROLE);
094        _solrClient = _solrClientProvider.getReadClient();
095    }
096    
097    @Override
098    public void generate() throws IOException, SAXException, ProcessingException
099    {
100        Request request = ObjectModelHelper.getRequest(objectModel);
101        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
102        String siteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME);
103        String lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
104
105        // Fetch the needed parameters from the zone item
106        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
107
108        // Content types
109        String[] cTypes = serviceParameters.getValue("content-types");
110
111        // Pages
112        String[] pages = serviceParameters.getValue("search-by-pages");
113
114        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
115        if (!_cacheManager.hasTagCloud(currentWsp, zoneItem.getId()))
116        {
117            List<TagCloudItem> tagCloud = getTagCloudItems(siteName, lang, serviceParameters);
118            _cacheManager.addTagCloud(currentWsp, zoneItem.getId(), tagCloud);
119        }
120        
121        @SuppressWarnings("unchecked")
122        List<TagCloudItem> tagCloud = (List<TagCloudItem>) _cacheManager.getTagCloud(currentWsp, zoneItem.getId());
123        
124        contentHandler.startDocument();
125        XMLUtils.startElement(contentHandler, "tagCloud");
126        
127        if (!tagCloud.isEmpty())
128        {
129            // Limit the number of items
130            int length = Integer.MAX_VALUE; // Max number of tags
131            if (serviceParameters.hasValue("limit"))
132            {
133                long limit = serviceParameters.getValue("limit");
134                length = (int) limit;
135            }
136            
137            if (length == 0)
138            {
139                length = tagCloud.size();
140            }
141            int max = tagCloud.get(0).getOccurrenceCount();
142            int min = tagCloud.size() < length ? tagCloud.get(tagCloud.size() - 1).getOccurrenceCount() : tagCloud.get(length - 1).getOccurrenceCount(); 
143            
144            Iterator<TagCloudItem> tagCloudIt = tagCloud.iterator();
145            
146            int i = 0;
147            while (tagCloudIt.hasNext() && i < length)
148            {
149                saxTagCloudItem(tagCloudIt.next(), min, max);
150                i++;
151            }
152            
153            // Search form values
154            _saxFormParameters(cTypes, pages);
155            
156            // Search engine page
157            String pageId = serviceParameters.getValue("search-engine-page", false, null);
158            if (StringUtils.isNotEmpty(pageId))
159            {
160                Page page = _resolver.resolveById(pageId);
161                XMLUtils.createElement(contentHandler, "searchUrl", page.getSitemapName() + "/" + page.getPathInSitemap());
162            }
163        }
164        
165        XMLUtils.endElement(contentHandler, "tagCloud");
166        contentHandler.endDocument();
167    }
168    
169    /**
170     * Get the tag cloud items
171     * @param siteName The site name
172     * @param lang The language
173     * @param serviceParameters The service parameters
174     * @return The list og tag cloud item
175     * @throws IOException if an error occurs when manipulating files
176     * @throws ProcessingException if an error occurs during the retrieving of the tag cloud items
177     */
178    protected abstract List<TagCloudItem> getTagCloudItems(String siteName, String lang, ModelAwareDataHolder serviceParameters) throws IOException, ProcessingException;
179    
180    /** 
181     * Sax a tag cloud item
182     * @param item The tag cloud item to sax
183     * @param min The min number of occurrence
184     * @param max The max number of occurrence
185     * @throws SAXException if an error occurs while saxing
186     */
187    protected void saxTagCloudItem(TagCloudItem item, int min, int max) throws SAXException
188    {
189        int nbOccurence = item.getOccurrenceCount();
190        int position = item.getPosition();
191        int fontSize = _getFontSize(nbOccurence, min, max);
192        
193        AttributesImpl attrs = new AttributesImpl();
194        attrs.addCDATAAttribute("nb", String.valueOf(nbOccurence));
195        attrs.addCDATAAttribute("original-position", String.valueOf(position));
196        attrs.addCDATAAttribute("font-size", String.valueOf(fontSize));
197        attrs.addCDATAAttribute("frequency", String.valueOf(fontSize + 1));
198        _saxAdditionalAttributes(item, attrs);
199        
200        XMLUtils.startElement(contentHandler, "item", attrs);
201        
202        List<String> i18nParams = new ArrayList<>();
203        i18nParams.add(String.valueOf(fontSize + 1));
204        new I18nizableText("plugin.tagcloud", "PLUGINS_TAGCLOUD_TAGS_SERVICE_FREQUENCY", i18nParams).toSAX(contentHandler, "frequency");
205        
206        item.getWord().toSAX(contentHandler);
207        
208        XMLUtils.endElement(contentHandler, "item");
209    }
210    
211    /**
212     * Build a {@link SolrQuery} from a Query object.
213     * @param query the Query object.
214     * @return The SolrQuery.
215     * @throws QuerySyntaxException if an error occurs.
216     */
217    protected SolrQuery build(Query query) throws QuerySyntaxException
218    {
219        String queryString = query.build();
220        
221        SolrQuery solrQuery = new SolrQuery(queryString);
222        
223        solrQuery.addFilterQuery("_documentType:\"" + SolrWebFieldNames.TYPE_PAGE + "\"");
224        
225        // Don't return any rows: we just need the result count.
226        solrQuery.setRows(0);
227        
228        return solrQuery;
229    }
230    
231    /**
232     * Sax additional attributes for item
233     * @param item The tag cloud item
234     * @param attrs The attributes
235     * @throws SAXException if an error occurs while saxing
236     */
237    protected void _saxAdditionalAttributes(TagCloudItem item, AttributesImpl attrs) throws SAXException
238    {
239        // Nothing
240    }
241    
242    /**
243     * Add content types term query to the queries.
244     * @param queries The query collection.
245     * @param cTypes the content types. Can be empty or null
246     */
247    protected void _addContentTypeQuery(Collection<Query> queries, String[] cTypes)
248    {
249        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
250        {
251            queries.add(new PageContentQuery(new ContentTypeQuery(cTypes)));
252        }
253    }
254    
255    /**
256     * Add pages term query to the queries
257     * @param queries The query collection.
258     * @param pageIds The page IDs.
259     */
260    protected void _addPagesQuery(Collection<Query> queries, String[] pageIds)
261    {
262        if (pageIds != null && pageIds.length > 0 && !(pageIds.length == 1 && pageIds[0].equals("")))
263        {
264            List<Query> pageQueries = new ArrayList<>();
265            
266            for (String pageId : pageIds)
267            {
268                pageQueries.add(new PageQuery(pageId, true));
269            }
270            
271            queries.add(new OrQuery(pageQueries));
272        }
273    }
274    
275    /**
276     * SAX teh form search criteria
277     * @param cTypes the content types
278     * @param pages the pages
279     * @throws SAXException if an error occurred while SAXing
280     */
281    protected void _saxFormParameters (String[] cTypes, String[] pages) throws SAXException
282    {
283        XMLUtils.startElement(contentHandler, "form");
284        
285        _saxContentTypeCriteria(cTypes);
286        _saxSitemapCriteria(pages);
287        
288        XMLUtils.endElement(contentHandler, "form");
289    }
290
291    /**
292     * Get the font size
293     * @param nb the number of occurrence
294     * @param min the min number of occurrence
295     * @param max the max number of occurrence
296     * @return the font size
297     */
298    protected int _getFontSize (int nb, int min, int max)
299    {
300        double p = ((double) nb - (double) min) / ((double) max - (double) min) * 100.0;
301        double interval = 100.0 / 5.0; // 6 sizes
302        
303        return (int) Math.floor(p / interval);
304    }
305
306    
307    /**
308     * SAX the content types criteria
309     * @param cTypes the content types
310     * @throws SAXException if an error occurred while SAXing
311     */
312    protected void _saxContentTypeCriteria (String[] cTypes) throws SAXException
313    {
314        XMLUtils.startElement(contentHandler, "content-types");
315        
316        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
317        {
318            for (String cTypeId : cTypes)
319            {
320                AttributesImpl attr = new AttributesImpl();
321                attr.addCDATAAttribute("id", cTypeId);
322                XMLUtils.createElement(contentHandler, "type", attr);
323            }
324        }
325        
326        XMLUtils.endElement(contentHandler, "content-types");
327    }
328    
329    /**
330     * SAX the pages criteria
331     * @param pages the pages
332     * @throws SAXException if an error occurred while SAXing
333     */
334    protected void _saxSitemapCriteria (String[] pages)  throws SAXException
335    {
336        XMLUtils.startElement(contentHandler, "pages");
337        
338        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
339        {
340            for (String pageID : pages)
341            {
342                Page page = _resolver.resolveById(pageID);
343                AttributesImpl attr = new AttributesImpl();
344                attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap());
345                attr.addCDATAAttribute("title", page.getTitle());
346                attr.addCDATAAttribute("id", pageID);
347                XMLUtils.createElement(contentHandler, "page", attr);
348            }
349        }
350        
351        XMLUtils.endElement(contentHandler, "pages");
352    }
353    
354//    /**
355//     * Returns the analyzer to use
356//     * @param language The current language
357//     * @return The {@link Analyzer}
358//     */
359//    protected Analyzer getAnalyzer (String language)
360//    {
361//        if (language.equals("br"))
362//        {
363//            return new BrazilianAnalyzer(LuceneConstants.LUCENE_VERSION);
364//        }
365//        else if (language.equals("cn"))
366//        {
367//            return new ChineseAnalyzer();
368//        }
369//        else if (language.equals("cz"))
370//        {
371//            return new CzechAnalyzer(LuceneConstants.LUCENE_VERSION);
372//        }
373//        else if (language.equals("gr"))
374//        {
375//            return new GreekAnalyzer(LuceneConstants.LUCENE_VERSION);
376//        }
377//        else if (language.equals("de"))
378//        {
379//            return new GermanAnalyzer(LuceneConstants.LUCENE_VERSION);
380//        }
381//        else if (language.equals("fr"))
382//        {
383//            return new FrenchAnalyzer(LuceneConstants.LUCENE_VERSION);
384//        }
385//        else if (language.equals("nl"))
386//        {
387//            return new DutchAnalyzer(LuceneConstants.LUCENE_VERSION);
388//        }
389//        else if (language.equals("ru"))
390//        {
391//            return new RussianAnalyzer(LuceneConstants.LUCENE_VERSION);
392//        }
393//        else if (language.equals("th"))
394//        {
395//            return new ThaiAnalyzer(LuceneConstants.LUCENE_VERSION);
396//        }
397//        else
398//        {
399//            //Default to standard analyzer (works well for english)
400//            return new StandardAnalyzer(LuceneConstants.LUCENE_VERSION);
401//        }
402//    }
403    
404//    /**
405//     * Get the index searcher
406//     * 
407//     * @param siteName The site to search in.
408//     * @param lang The current language
409//     * @return The index searcher
410//     * @throws IOException If an error occurred while opening indexes
411//     */
412//    protected Searcher getSearchIndex(String siteName, String lang) throws IOException
413//    {
414//        File indexDir = IndexerHelper.getIndexDir(_context, siteName, lang);
415//        if (indexDir.exists() && indexDir.listFiles().length > 0)
416//        {
417//            return new IndexSearcher(FSDirectory.open(indexDir));
418//        }
419//        else
420//        {
421//            return null;
422//        }
423//    }
424    
425    /**
426     * Abstract class for a tag cloud item
427     *
428     */
429    protected interface TagCloudItem
430    {
431        /**
432         * Get the tag cloud word to display
433         * @return The tag cloud word
434         */
435        I18nizableText getWord();
436        
437        /**
438         * Returns the number of occurrence
439         * @return the number of occurrence
440         */
441        int getOccurrenceCount();
442        
443        /**
444         * Get the original position.
445         * @return the original position.
446         */
447        int getPosition();
448    }
449    
450    /**
451     * Compares two terms by descending occurrence count.
452     */
453    protected static class ItemOccurrenceComparator implements Comparator<TagCloudItem>
454    {
455        
456        @Override
457        public int compare(TagCloudItem tc1, TagCloudItem tc2)
458        {
459            return tc2.getOccurrenceCount() - tc1.getOccurrenceCount();
460        }
461        
462    }
463    
464}