// 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: Template.java,v 1.11 2008/04/15 10:29:11 spyromus Exp $
//
package com.salas.bb.remixfeeds.templates;
import com.salas.bb.domain.DirectFeed;
import com.salas.bb.domain.IArticle;
import com.salas.bb.domain.IFeed;
import com.salas.bb.domain.StandardArticle;
import com.salas.bb.utils.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.text.DateFormat;
import java.util.*;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;
/**
* A template.
*/
public class Template
{
private static final Pattern PATTERN_IF = Pattern.compile("^\\s*#\\s+if\\s+single(\\s+article)?\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ELSE = Pattern.compile("^\\s*#\\s+else\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ENDIF = Pattern.compile("^\\s*#\\s+endif\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_FOR_EACH = Pattern.compile("^\\s*#\\s+for\\s+each\\s+article\\s*$", Pattern.CASE_INSENSITIVE);
private static final Pattern PATTERN_ENDFOR = Pattern.compile("^\\s*#\\s+endfor\\s*$", Pattern.CASE_INSENSITIVE);
private static final String PATTERN_FEED_TITLE = "${feed.title}";
private static final String PATTERN_FEED_URL = "${feed.url}";
private static final String PATTERN_ARTICLE_TITLE = "${article.title}";
private static final String PATTERN_ARTICLE_URL = "${article.url}";
private static final String PATTERN_ARTICLE_TEXT = "${article.text}";
private static final String PATTERN_ARTICLE_BRIEF_TEXT = "${article.brief-text}";
private static final String PATTERN_ARTICLE_DATE = "${article.date}";
/** Template name. */
private String name;
/** System or user-defined template. */
private boolean system;
/** Template text. */
private String text;
/**
* Creates an empty template.
*/
Template()
{
}
/**
* Creates a template.
*
* @param name name.
* @param system system flag.
* @param text string text.
*/
public Template(String name, boolean system, String text)
{
this.name = name;
this.system = system;
this.text = text;
}
/**
* Returns the name.
*
* @return name.
*/
public String getName()
{
return name;
}
/**
* Sets the name of the template.
*
* @param name name.
*/
private void setName(String name)
{
this.name = name;
}
/**
* Sets the template text.
*
* @param text text.
*
* @throws InvalidSyntaxException if template syntax is incorrect.
*/
public void setText(String text)
throws InvalidSyntaxException
{
List<SyntaxError> errors = SyntaxChecker.validate(text);
if (!errors.isEmpty()) throw new InvalidSyntaxException(errors);
this.text = text;
}
/**
* Returns current template text.
*
* @return text.
*/
public String getText()
{
return text;
}
/**
* Renders a single article.
*
* @param article article.
* @param selectedText selected text or NULL.
*
* @return HTML.
*/
public String render(IArticle article, String selectedText)
{
// Wrap article to override the text
if (StringUtils.isNotEmpty(selectedText)) article = new ArticleWrapper(article, selectedText);
ArrayList<IArticle> list = new ArrayList<IArticle>(1);
list.add(article);
return render(list);
}
/**
* Renders a template.
*
* @param articles articles.
*
* @return HTML.
*/
public String render(Collection<IArticle> articles)
{
String result = "";
boolean single = articles.size() == 1;
if (StringUtils.isNotEmpty(text))
{
try
{
BufferedReader reader = new BufferedReader(new StringReader(text));
result = render(reader, articles, single);
} catch (IOException e)
{
// Never happens as we deal with a string
}
}
return result;
}
/**
* Analyzes the line and renders it if it's a text line or starts the sequence, if it's
* an operation.
*
* @param reader reader.
* @param articles articles.
* @param single TRUE if it's a single-article template.
*
* @return result.
*
* @throws IOException if fails.
*/
private String render(BufferedReader reader, Collection<IArticle> articles, boolean single)
throws IOException
{
String line;
String result = "";
boolean skip = false;
while ((line = reader.readLine()) != null)
{
if (line.matches("\\s*#.+"))
{
// Operation
if (!skip && PATTERN_IF.matcher(line).matches())
{
result += renderIf(reader, articles, single);
} else if (!skip && PATTERN_FOR_EACH.matcher(line).matches())
{
result += renderForEach(reader, articles);
} else if (PATTERN_ENDIF.matcher(line).matches() ||
PATTERN_ENDFOR.matcher(line).matches())
{
// Break the loop if we encounter the end of the section of any kind
break;
} else if (PATTERN_ELSE.matcher(line).matches())
{
// If else block is reached, we need to skip it
skip = true;
}
} else if (!skip)
{
result += applyPatterns(line, articles.iterator().next());
}
}
return result;
}
private String renderForEach(BufferedReader reader, Collection<IArticle> articles)
throws IOException
{
String result = "";
List<String> lines = new LinkedList<String>();
// Fetch in all lines
String line;
while ((line = reader.readLine()) != null)
{
if (PATTERN_ENDFOR.matcher(line).matches())
{
break;
} else
{
lines.add(line);
}
}
// For each article, output lines
for (IArticle article : articles)
{
for (String l : lines)
{
result += applyPatterns(l, article);
}
}
return result;
}
private String renderIf(BufferedReader reader, Collection<IArticle> articles, boolean single)
throws IOException
{
String results = "";
String line;
if (single)
{
// Single article is available
results += render(reader, articles, single);
} else
{
// Multi-article mode
while ((line = reader.readLine()) != null)
{
if (PATTERN_ELSE.matcher(line).matches())
{
// Start processing
results += render(reader, articles, single);
break;
} else if (PATTERN_ENDIF.matcher(line).matches())
{
break;
}
}
}
return results;
}
/**
* Applies patterns to the line and returns the result.
*
* @param line line.
* @param article article.
*
* @return result.
*/
private String applyPatterns(String line, IArticle article)
{
if (StringUtils.isEmpty(line)) return line + "\n";
IFeed feed = article.getFeed();
if (feed != null)
{
line = line.replace(PATTERN_FEED_TITLE, feed.getTitle());
if (feed instanceof DirectFeed)
{
DirectFeed dFeed = (DirectFeed)feed;
line = line.replace(PATTERN_FEED_URL, toString(dFeed.getXmlURL()));
}
}
line = line.replace(PATTERN_ARTICLE_TITLE, article.getTitle());
line = line.replace(PATTERN_ARTICLE_DATE, toString(article.getPublicationDate()));
line = line.replace(PATTERN_ARTICLE_TEXT, article.getHtmlText());
line = line.replace(PATTERN_ARTICLE_BRIEF_TEXT, article.getBriefText());
line = line.replace(PATTERN_ARTICLE_URL, toString(article.getLink()));
return line + "\n";
}
/**
* Safely converts a date into string.
*
* @param date date.
*
* @return string.
*/
private String toString(Date date)
{
return date == null ? "" : DateFormat.getDateInstance(DateFormat.MEDIUM).format(date);
}
/**
* Safely converts an URL into string.
*
* @param url URL.
*
* @return string.
*/
private String toString(URL url)
{
return url == null ? "" : url.toString();
}
/**
* Returns string representation.
*
* @return string.
*/
public String toString()
{
return name;
}
/**
* Returns TRUE if template is system-template.
*
* @return TRUE if template is system-template.
*/
public boolean isSystem()
{
return system;
}
/**
* Stores a template in the preferences.
*
* @param prefs preferences.
* @param seq sequence number.
*/
public void store(Preferences prefs, int seq)
{
String n = Integer.toString(seq);
prefs.put("ptb.template." + n + ".name", getName());
prefs.put("ptb.template." + n + ".text", getText());
}
/**
* Restores a template from the preferences.
*
* @param prefs preferences.
* @param seq sequence number.
*/
public void restore(Preferences prefs, int seq)
{
String n = Integer.toString(seq);
setName(prefs.get("ptb.template." + n + ".name", null));
setText(prefs.get("ptb.template." + n + ".text", ""));
}
/**
* Article wrapper that is used for article text overriding.
*/
private static class ArticleWrapper extends StandardArticle
{
private IArticle article;
private ArticleWrapper(IArticle article, String selectedText)
{
super(selectedText);
this.article = article;
}
@Override
public String getHtmlText()
{
return getText();
}
@Override
public synchronized String getPlainText()
{
return getText();
}
@Override
public String getAuthor()
{
return article.getAuthor();
}
@Override
public Date getPublicationDate()
{
return article.getPublicationDate();
}
@Override
public synchronized String getTitle()
{
return article.getTitle();
}
@Override
public URL getLink()
{
return article.getLink();
}
@Override
public IFeed getFeed()
{
return article.getFeed();
}
@Override
public String getBriefText()
{
return getText();
}
}
}