/*
* Copyright (c) 1999-2002 ChurchillObjects.com All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met: Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. Neither the name of the copyright
* holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT, INCLUDING NEGLIGENCE OR OTHERWISE, ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
*/
package churchillobjects.rss4j.generator;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
//import org.apache.xml.serialize.OutputFormat;
//import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import churchillobjects.rss4j.RssChannel;
import churchillobjects.rss4j.RssDocument;
import churchillobjects.rss4j.RssJbnDependency;
import churchillobjects.rss4j.RssJbnPatch;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
/**
* A base and factory class for RSS generator objects. From a version number, will
* construct an appropriate implementation and generate the RSS document.
*/
public abstract class RssGenerator {
/**
* The DOM implementation used by the XML toolkit (user-supplied, but best
* to use Apache's Xerces).
*/
protected DOMImplementation domImpl;
/**
* An XML formatter object for the document when it is serialized.
*/
protected OutputFormat format;
/**
* The XML document, used as it is being built.
*/
protected Document doc;
protected Writer outputWriter;
protected OutputStream outputStream;
protected Element rootElement;
private boolean truncateText = false;
// for enforcement of max lengths, used by 0.90 and 0.91, but not 1.0
protected int channelTitleMax;
protected int channelDescriptionMax;
protected int channelLinkMax;
protected int channelPubDate;
protected int channelBuildDate;
protected int channelManagingEditorMax;
protected int channelWebmasterMax;
protected int channelCopyrightMax;
protected int channelDocsMax;
protected int imageTitleMax;
protected int imageUrlMax;
protected int imageLinkMax;
protected int imageDescriptionMax;
protected int itemTitleMax;
protected int itemLinkMax;
protected int itemDescriptionMax;
protected int textInputTitleMax;
protected int textInputDescriptionMax;
protected int textInputNameMax;
protected int textInputLinkMax;
/**
* Creates an appropriate instance of RssGenerator with a StringWriter and
* generates the RSS into the writer, then returns the XML as a string.
* @param document
* @return
*/
public static String generateRss(RssDocument document) throws RssGenerationException{
StringWriter sw = new StringWriter();
generateRss(document, sw);
return sw.toString();
}
/**
* Creates an appropriate instance of RssGenerator and has it write the
* RSS code to the specified output stream.
* @param document
* @param output
* @throws RssGenerationException
*/
public static void generateRss(RssDocument document, OutputStream output) throws RssGenerationException{
RssGenerator generator = getGenerator(document);
generator.writeRssDocument(document, output);
}
/**
* Creates an appropriate instance of RssGenerator and has it write the
* RSS code to the specified file object.
* @param document
* @param file
* @throws RssGenerationException
*/
public static void generateRss(RssDocument document, File file) throws RssGenerationException{
RssGenerator generator = getGenerator(document);
generator.writeRssDocument(document, file);
}
/**
* Creates an appropriate instance of RssGenerator and has it write the
* RSS code to the specified output writer.
* @param document
* @param output
* @throws RssGenerationException
*/
public static void generateRss(RssDocument document, Writer output) throws RssGenerationException{
RssGenerator generator = getGenerator(document);
generator.writeRssDocument(document, output);
}
/**
* Returns the appropriate generator for the specified RSS document object.
* If none, then a RSSGenerationException is thrown.
* @param document
* @return
*/
private static RssGenerator getGenerator(RssDocument document) throws RssGenerationException{
String version = document.getVersion();
if(version==null || version.length()==0){
throw new RssGenerationException("RSS version was not specified in the RssDocument object.");
}
if("0.90".equals(version)){
return new RssGeneratorImpl090();
}
else if("0.91".equals(version)){
return new RssGeneratorImpl091();
}
else if("1.0".equals(version)){
return new RssGeneratorImpl100();
}
throw new RssGenerationException("Could not find a generator for the document version: " + version);
}
/**
* Constructor. Sets up the XML document
* and formatter, then invokes setMaxLengths.
*/
RssGenerator() throws RssGenerationException{
try{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
domImpl = builder.getDOMImplementation();
format = new OutputFormat();
format.setLineWidth(65);
format.setIndenting(true);
format.setIndent(2);
format.setEncoding("UTF-8");
format.setMediaType("application/xml");
format.setOmitComments(true);
format.setOmitXMLDeclaration(false);
format.setVersion("1.0");
format.setStandalone(true);
}
catch (ParserConfigurationException e) {
throw new RssGenerationException("Could not locate a JAXP DocumentBuilder class");
}
setMaxLengths();
}
/**
* Implemented by subclasses. If there are field length limitations that need
* to be adhered to, then the version implementation would set them here and
* it will be invoked at initialization time.
*/
protected abstract void setMaxLengths();
protected abstract void handleChannel(RssChannel channel) throws RssGenerationException;
protected abstract void finishDocument() throws RssGenerationException;
/**
* Serializes the completed DOM structure to the source given by the client code.
* Any problems here are wrapped in an RssGenerationException and thrown up the
* stack.
* @param data
* @throws RssGenerationException
*/
private void writeRssDocument(RssDocument data) throws RssGenerationException{
try{
createRssDocument(data);
Enumeration channels = data.channels();
while(channels.hasMoreElements()){
RssChannel channel = (RssChannel)channels.nextElement();
if(channel!=null){
handleChannel(channel);
}
}
finishDocument();
XMLSerializer serializer = null;
if(outputWriter!=null){
serializer = new XMLSerializer(outputWriter, format);
}
else{
serializer = new XMLSerializer(outputStream, format);
}
serializer.serialize(doc);
}
catch (FactoryConfigurationError e) {
throw new RssGenerationException("Could not locate a JAXP factory class");
}
catch (DOMException e) {
throw new RssGenerationException(e);
}
catch (IOException e) {
throw new RssGenerationException(e);
}
}
/**
* Subclass implementation for creating the DOM from the specified RSS document
* object model.
* @param data
* @throws RssGenerationException
*/
protected abstract void createRssDocument(RssDocument data) throws RssGenerationException;
/**
* Overloaded write method fo a Java IO output stream. The resulting RSS code
* will be written to the specified output stream.
* @param data
* @param output
* @throws RssGenerationException
*/
private void writeRssDocument(RssDocument data, OutputStream output) throws RssGenerationException{
outputStream = output;
writeRssDocument(data);
}
/**
* Overloaded write method for a Java File object. The resulting RSS code will be
* written to the specified file.
* @param data
* @param file
* @throws RssGenerationException
*/
private void writeRssDocument(RssDocument data, File file) throws RssGenerationException{
try{
outputStream = new FileOutputStream(file);
writeRssDocument(data);
}
catch(IOException e){
throw new RssGenerationException(e);
}
}
/**
* Overloaded write method for a Java IO writer object. The resulting RSS code
* will be written to the specified writer.
* @param data
* @param output
* @throws RssGenerationException
*/
private void writeRssDocument(RssDocument data, Writer output) throws RssGenerationException{
outputWriter = output;
writeRssDocument(data);
}
/**
* Convenience method that adds a new node to an existing XML element with
* the specified value. The method will not create and add the new node if
* the value is null.
* @param baseElement
* @param nodeName
* @param textValue
* @return
*/
protected Element add(Element baseElement, String nodeName, String textValue){
if(textValue!=null){
Element attrib = doc.createElement(nodeName);
Text chars = doc.createTextNode(textValue);
attrib.appendChild(chars);
baseElement.appendChild(attrib);
return attrib;
}
return null;
}
/**
* Convenience method that adds a new node with a child node to an existing XML element with the specified values.
* The method will not create and add the new node if the innerNodeValues AND the attributeValue is null.<br><br>
* <outerNodeName attributeName=attributeValue ><br>
* < nodeName > nodeValue1 </nodeName >
* < nodeName > nodeValue2 </nodeName >
* </outerNodeName >
* @param baseElement the base element that everything is added to
* @param outerNodeName the name for the outer node
* @param attributeName the name for the attribute of the outer node
* @param attributeValue the value for the attribute of the inner node
* @param innerNodeName the name for the inner node
* @param innerNodeValues the value(s) for the inner nodes.
* @return the base element
*/
protected Element add( Element baseElement, String outerNodeName, String attributeName, String attributeValue,
String innerNodeName, Collection innerNodeValues)
{
if( attributeValue != null || innerNodeValues != null )
{
Element outerNode = doc.createElement( outerNodeName );
if( attributeValue != null )
{
outerNode.setAttribute( attributeName, attributeValue);
}
if( innerNodeValues != null )
{
Iterator innerValues = innerNodeValues.iterator();
while( innerValues.hasNext() )
{
Element innerNode = doc.createElement( innerNodeName );
Text chars = doc.createTextNode( (String)innerValues.next() );
innerNode.appendChild( chars );
outerNode.appendChild( innerNode );
}
}
baseElement.appendChild( outerNode );
}
return baseElement;
}
/**
* Convenience method that adds a new node with a child node to an existing XML element with the specified values.
* The method will not create and add the new node if the innerNodeValues AND the attributeValue is null.<br><br>
* <outerNodeName ><br>
* < innerNodeName innerNodeAttributeName = dependencies[0].geUrl() > dependencies[0].getName() </innerNodeName ><br>
* < innerNodeName innerNodeAttributeName = dependencies[1].geUrl() > dependencies[1].getName() </innerNodeName ><br>
* </ outerNodeName ><br>
* <br>ex:<br>
* <jbn:products><br>
* < jbn:product rdf:about=http://network.jboss.com?id=123 >JBossAS 4.0.2 ><br>
* < jbn:product rdf:about=http://network.jboss.com?id=456 >JBossAS 4.0.3 ><br>
* </ jbn:producs><br>
*
* @param baseElement the base element that everything is added to
* @param outerNodeName the name for the outer node
* @param innerNodeName the name for the inner node
* @param innerNodeAttributeName the name for the attribute of the inner node
* @param dependencies the dependency.getUrl will be the attribute Value and the dependency.getName will be the node value
* @return
*/
protected Element add( Element baseElement, String outerNodeName, String innerNodeName,
String innerNodeAttributeName, Collection dependencies )
{
//TODO add tests for this method
// -- check for nulls
// -- make sure exception is thrown correctly
// -- verify various correct formats are produced (with and without nulls)
if( innerNodeName != null && dependencies != null )
{
Element outerNode = doc.createElement( outerNodeName );
Iterator iterator = dependencies.iterator();
while( iterator.hasNext() )
{
Element innerNode = doc.createElement( innerNodeName );
RssJbnDependency dependency = (RssJbnDependency)iterator.next();
if( innerNodeAttributeName != null && dependency != null )
{
innerNode.setAttribute( innerNodeAttributeName, dependency.getUrl() );
}
Text chars = doc.createTextNode( dependency.getName() );
innerNode.appendChild( chars );
outerNode.appendChild( innerNode );
}
baseElement.appendChild( outerNode );
}
return baseElement;
}
/**
* this add method expects a collection of RssJbnDependency objects -- that have attributes for a jbn:product
* @param baseElement the base element this node will be added to
* @param outerNodeName the name for the outer node (should be JBN:PRODUCTS)
* @param innerNodeName the name for the inner node (should be JBN:PRODUCT)
* @param dependencies a collection of RssJbnDependency objects
* @return the base element
*/
protected Element add( Element baseElement, String outerNodeName, String innerNodeName, Collection dependencies )
{
Element outerNode = doc.createElement( outerNodeName );
if( innerNodeName != null && dependencies != null )
{
Iterator dependencyIterator = dependencies.iterator();
while( dependencyIterator.hasNext() )
{
RssJbnDependency rjd = (RssJbnDependency)dependencyIterator.next();
Map nameValuePair = new HashMap();
nameValuePair.put( "rdf:about", rjd.getUrl() );
nameValuePair.put( RssJbnPatch.PREFIX + ":" + RssJbnDependency.ATTR_PRODUCT_NAME, rjd.getProductName() );
nameValuePair.put( RssJbnPatch.PREFIX + ":" + RssJbnDependency.ATTR_PRODUCT_VERSION, rjd.getProductVersion() );
nameValuePair.put( RssJbnPatch.PREFIX + ":" + RssJbnDependency.ATTR_JON_RESOURCE_TYPE, rjd.getJonResourceType() );
nameValuePair.put( RssJbnPatch.PREFIX + ":" + RssJbnDependency.ATTR_JON_RESOURCE_VERSION, rjd.getJonResourceVersion() );
outerNode.appendChild(
add( RssJbnPatch.PREFIX + ":" + RssJbnPatch.ATTR_PRODUCT,
nameValuePair, rjd.getName() ) );
}
baseElement.appendChild( outerNode );
}
return baseElement;
}
/**
* creates an inner node based on the attribute name/value pair. This method is originally meant for the JBN:PRODUCT element
* @param innerNodeName the name of the inner node
* @param attributeNameValuePairs a HashMap of attribute name/value pairs
* @param content the content of the element
* @return the new inner node
*/
private Element add( String innerNodeName, Map attributeNameValuePairs, String content )
{
Element innerNode = doc.createElement( innerNodeName );
Iterator keyIterator = attributeNameValuePairs.keySet().iterator();
while( keyIterator.hasNext() )
{
String name = (String)keyIterator.next();
String value = (String)attributeNameValuePairs.get( name );
innerNode.setAttribute( name, value );
}
Text chars = doc.createTextNode( content );
innerNode.appendChild( chars );
return innerNode;
}
/**
* Set this accessor to TRUE if truncation of text is desired. This will truncate
* any text that is truncatable (titles, descriptions, etc) and append an ellipsis
* (three dots) at the end of the text to show that there is more. This only
* applies if the text would otherwise exceed the limit of the field during
* generation (and thus will not change the object that contains the too-long
* text). Applies to RSS 0.90 and 0.91 specifications.
* @param b Truncate too-long text during RSS generation, if the standard warrants it.
*/
public void setTruncateText(boolean b){
truncateText = b;
}
/**
* Inidcates that truncation is on or off, whereby certain text (titles, descriptions,
* etc. will be truncated if they exceed the RSS standard being used.
* @return If text will be truncated if it happens to be too long for the standard.
*/
public boolean isTruncateText(){
return truncateText;
}
/**
*
* @param s The string value to truncate. If null, then nothing is done.
* @param length The number of characters to truncate to. If zero, then there is
* no limit on the length of the element.
* @return The truncated (if necessary) string, or null if s is null.
* @throws RssGenerationException
*/
protected String truncate(String s, int length) throws RssGenerationException{
if(!truncateText || s==null || length==0){
return s;
}
s = s.trim();
if(s.length() <= length){
return s;
}
s = s.substring(0,length-3).trim();
return s+"...";
}
/**
* Validates a value for a field that is required, checking its minimum and
* maximum lengths. If the value is null, has no length or exceeds the required
* length (if any), then an RssGenerationException is thrown up the stack.
* the test passes.
* @param value
* @param name
* @param minLength
* @param maxLength
* @throws RssGenerationException
*/
protected void validateValueRequired(String value, String name, int minLength, int maxLength) throws RssGenerationException{
if(value==null || value.length()==0){
throw new RssGenerationException(name + " attribute required");
}
if(value==null || value.length()==0 || (maxLength>0 && value.length()>maxLength)){
throw new RssGenerationException(name + " attribute must be between " + minLength + " and " + maxLength + " characters: " + value.length() + " '" + value + "'");
}
}
/**
* Overloaded version of validateValueRequired, for when there is no minimum
* length for the field (1 will be used for minimum length).
* @param value
* @param name
* @param maxLength
* @throws RssGenerationException
*/
protected void validateValueRequired(String value, String name, int maxLength) throws RssGenerationException{
validateValueRequired(value, name, 1, maxLength);
}
/**
* Validates a value for a field that is not required, checking its minimum and
* maximum lengths. If the value is null or there is no maximum (not > 0), then
* the test passes.
* @param value
* @param name
* @param minLength
* @param maxLength
* @throws RssGenerationException
*/
protected void validateValueOptional(String value, String name, int minLength, int maxLength) throws RssGenerationException{
if(maxLength>0 && value!=null && value.length()>maxLength){
throw new RssGenerationException(name + " optional, but length must be between " +
minLength + " and " + maxLength + " characters: " + value.length() + " '" + value + "'");
}
}
/**
* Overloaded version of validateValueOptional, for when there is no minimum
* length for the field (1 will be used for minimum length).
* @param value
* @param name
* @param maxLength
* @throws RssGenerationException
*/
protected void validateValueOptional(String value, String name, int maxLength) throws RssGenerationException{
validateValueOptional(value, name, 1, maxLength);
}
/**
* For fields that may have HTML embedded in the code, adds a CDATA wrapper for
* the text value so that XML validators will not blow up.
* @param s The text to be evaluated
* @return The modified text, or original if not modified
*/
protected String embedHtml(String s){
if(s.indexOf("<")>=0){
s = "<[!CDATA[" + s + "]]>";
}
return s;
}
/**
* For fields that must have valid URIs, checks to see that the field contains one of
* the four valid URI prefixes (http, https, ftp or mailto).
* @param s The URI value to evaluate
* @throws RssGenerationException When the validation fails
*/
protected void validateUri(String s) throws RssGenerationException{
if(s!=null && s.length()>0 && !(
s.startsWith("http:") ||
s.startsWith("https:") ||
s.startsWith("ftp:") ||
s.startsWith("mailto:")
)){
throw new RssGenerationException(
"Invalid URI format: must be 'http:', 'https:', 'ftp:', or 'mailto:' - " + s);
}
}
}