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