// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: HighlightsCalculator.java,v 1.38 2007/11/07 17:16:48 spyromus Exp $
//
package com.salas.bb.core;
import com.salas.bb.domain.IArticle;
import com.salas.bb.domain.IFeed;
import com.salas.bb.domain.utils.TextRange;
import com.salas.bb.utils.Constants;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.concurrency.CachingCalculator;
import com.salas.bb.utils.swinghtml.TextProcessor;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Calculator of highlighted areas for articles. Responsible for providing always actual
* information about what should be highlighted in articles.
*/
public class HighlightsCalculator
{
private Map rangesCache = Collections.synchronizedMap(new WeakHashMap());
private Map countsCache = Collections.synchronizedMap(new WeakHashMap());
private String currentKeywords;
private Pattern pattern = null;
private final Object patternLock = new Object();
private final Calculator calculator;
/**
* Creates highlights calculator.
*/
public HighlightsCalculator()
{
calculator = new Calculator(1);
}
/**
* Returns list of highlighted ranges in article.
*
* @param text text of article.
*
* @return list of regions.
*/
public TextRange[] getHighlights(String text)
{
if (text == null) return Constants.EMPTY_TEXT_RANGE_LIST;
// TODO !!!! read/write locking necessary !!!!
CustomCacheKey key = CustomCacheKey.key(text);
TextRange[] tranges = (TextRange[])rangesCache.get(key);
if (tranges == null)
{
tranges = TextRange.findRanges(text, getKeywordsPattern());
if (tranges != null) rangesCache.put(key, tranges);
}
return tranges;
}
/**
* Returns number of highlights in text. Uses simplified scheme.
*
* @param text text to process.
*
* @return number of highlights.
*/
public int getHighlightsCount(String text)
{
if (text == null) return 0;
int cnt = 0;
String plainText = TextProcessor.toPlainText(text);
Integer count = getCountFromCache(plainText);
if (count == null)
{
Pattern pat = getKeywordsPattern();
if (pat != null)
{
Matcher mat = pat.matcher(plainText);
int st = 0;
while (mat.find(st))
{
cnt++;
st = mat.end(2);
}
}
countsCache.put(CustomCacheKey.key(plainText), cnt);
} else
{
cnt = count.intValue();
}
return cnt;
}
private Integer getCountFromCache(String text)
{
return (Integer)countsCache.get(CustomCacheKey.key(text));
}
/**
* Returns regex pattern with keywords.
*
* @return pattern.
*/
private Pattern getKeywordsPattern()
{
Pattern pat;
synchronized (patternLock)
{
pat = pattern;
}
return pat;
}
/**
* Called to notify that keywords has changed.
*
* @param newKeywords new keywords.
*/
public void keywordsChanged(String newKeywords)
{
boolean invalidate = false;
synchronized (patternLock)
{
if ((currentKeywords == null && newKeywords != null) ||
(currentKeywords != null && !currentKeywords.equals(newKeywords)))
{
currentKeywords = newKeywords;
String patternRegex = StringUtils.keywordsToPattern(currentKeywords);
pattern = patternRegex == null ? null
: Pattern.compile(patternRegex, Pattern.CASE_INSENSITIVE);
invalidate = true;
}
}
if (invalidate) invalidateAll();
}
/**
* Returns number of highlighted regions in articles of this feed.
*
* @param feed feed to check.
*
* @return number of highlights.
*/
public int getHighlightsCount(IFeed feed)
{
return ((CompositeValue)calculator.getValue(feed)).highlights;
}
/**
* Returns number of highlighted articles.
*
* @param feed feed to examine.
*
* @return number of articles with highlights.
*/
public int getHighlightedArticles(IFeed feed)
{
return ((CompositeValue)calculator.getValue(feed)).articlesWithHighlights;
}
/**
* Invalidates all records stored in cache.
*/
public void invalidateAll()
{
synchronized (this)
{
rangesCache.clear();
countsCache.clear();
}
calculator.invalidateAll();
}
/**
* Invalidates the feed stored in cache.
*
* @param feed feed to invalidate.
*/
public void invalidateFeed(IFeed feed)
{
calculator.invalidateKey(feed);
}
/**
* Removes the feed from cache.
*
* @param feed feed.
*/
public void feedRemoved(IFeed feed)
{
calculator.removeKey(feed);
}
/**
* Returns number of highlights for given article.
*
* @param aArticle article.
*
* @return number of highlights.
*/
public int getHighlightsCount(IArticle aArticle)
{
return getHighlightsCount(aArticle.getPlainText());
}
/**
* Simple composite value holder.
*/
private static class CompositeValue
{
private volatile int highlights;
private volatile int articlesWithHighlights;
}
private static class CustomCacheKey
{
private volatile int length;
private volatile int hash;
private CustomCacheKey(int length, int hash)
{
this.length = length;
this.hash = hash;
}
public static CustomCacheKey key(String s)
{
return s == null ? new CustomCacheKey(0, 0) : new CustomCacheKey(s.length(), s.hashCode());
}
@Override
public int hashCode()
{
return hash;
}
@Override
public boolean equals(Object o)
{
boolean res = false;
if (o instanceof CustomCacheKey)
{
CustomCacheKey other = (CustomCacheKey)o;
res = length == other.length && hash == other.hash;
}
return res;
}
}
/**
* Calculator of highligts counts.
*/
private class Calculator extends CachingCalculator
{
public Calculator(int threads)
{
super(threads);
startThreads();
}
protected String getThreadsBaseName()
{
return "HC";
}
protected Object calculate(Object key)
{
final IFeed feed = (IFeed)key;
CompositeValue value = new CompositeValue();
int highlights = 0;
int articlesWithHighlights = 0;
IArticle[] articles = feed.getArticles();
for (int i = 0; i < articles.length; i++)
{
IArticle article = articles[i];
int highlightsInArticle = getHighlightsCount(article.getPlainText());
highlights += highlightsInArticle;
if (highlightsInArticle > 0) articlesWithHighlights++;
}
value.articlesWithHighlights = articlesWithHighlights;
value.highlights = highlights;
return value;
}
}
}