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}