package org.codehaus.mojo.delicious;
/*
* Copyright 2005 Ashley Williams.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.log4j.Logger;
import org.apache.log4j.Priority;
import org.xml.sax.SAXException;
/**
* A high level service access class for Delicious bookmarks.
* @author ashley
*
*/
public class DeliciousService
{
private static Logger logger = Logger.getLogger( DeliciousService.class );
private DeliciousConnection connection;
private String serviceUnavailableMessage = "delicious service is unavailable";
private DeliciousListener listener;
private String userName;
private String password;
private int code;
/**
* Returns true if the given exception represents the service unavailable exception.
* @param e
* @return
*/
public boolean isServiceUnavailableException( RuntimeException e )
{
return serviceUnavailableMessage.equals( e.getMessage() );
}
/**
* Plugin an alternative connection - probably for testing.
*/
public DeliciousService( DeliciousConnection connection )
{
this.connection = connection;
this.listener = createDefaultListener();
}
/**
* Creates a service that can connect to the REST api.
*/
public DeliciousService()
{
this( new LiveConnection() );
}
/**
* Creates the default listener that just logs as info.
* @return
*/
private DeliciousListener createDefaultListener()
{
return new DeliciousListener()
{
public void serviceDone( int code, Reader responseBody )
throws IOException
{
logger.info( new Integer( code ) );
Util.logStream( logger, Priority.INFO, responseBody );
}
};
}
/**
* Gets the currently set listener.
*
* @return
*/
public DeliciousListener getListener()
{
return listener;
}
/**
* Currently sets the one and only listener.
* @param listener
*/
public void setListener( DeliciousListener listener )
{
this.listener = listener;
}
/**
* The code of the most recent server communication.
* @return
*/
public int getCode()
{
return code;
}
/**
* Adds the links found at the given reader to the delicious service.
* The reader is assumed to be for a valid xml file containing anchor
* tags.
*
* @param links
* @param replace
* @throws IOException
* @throws InterruptedException
*/
public void addBookmarks( Reader links, Boolean replace )
throws IOException, InterruptedException
{
addBookmarks( new BookmarkParser().parse( links, new BookmarkGroup() ), replace );
}
/**
* Adds the given group of bookmarks.
* @param group
* @param replace
* @throws IOException
* @throws InterruptedException
*/
public void addBookmarks( BookmarkGroup group, Boolean replace )
throws IOException, InterruptedException
{
addBookmarks( group.getBookmarks(), replace );
}
/**
* Adds the given list of bookmars to the delicious service.
* @param bookmarks
* @param replace
* @throws InterruptedException
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
* @throws InterruptedException
*/
public void addBookmarks( List bookmarks, Boolean replace )
throws IOException, InterruptedException
{
Iterator allBookmarks = bookmarks.iterator();
while ( allBookmarks.hasNext() )
{
Bookmark bookmark = (Bookmark) allBookmarks.next();
addBookmark( bookmark, replace );
}
}
/**
* Adds the given bookmark to the delicious service.
* @param bookmark
* @param replace TODO
* @throws IOException
* @throws InterruptedException
*/
public void addBookmark( Bookmark bookmark, Boolean replace )
throws IOException, InterruptedException
{
addPost( bookmark.getLocation(), bookmark.getTitle(), bookmark.getTags(), bookmark.getComments(), replace );
}
/**
* Adds the links found at the given path, that can be a local file or net URL.
* Convenience method.
* @param replace
* @param links
* @throws IOException
* @throws InterruptedException
*/
public void addBookmarks( String linksPage, Boolean replace )
throws IOException, InterruptedException
{
addBookmarks( Util.getReader( linksPage ), replace );
}
/**
* Sets the credential to be used for this service.
* @param userName
* @param password
*/
public void setUser( String userName, String password )
{
this.userName = userName;
this.password = password;
}
/**
* Fetches a list of dates with the number of posts at each date.
* @param tags
* @throws IOException
* @throws InterruptedException
*/
public void fetchDates( String tags )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
if ( tags != null )
{
formFields.put( "tag", tags );
}
doService( "posts", "dates", formFields );
}
/**
* Fetches a list of posts with the given search criteria.
* Either specify a tags/date combination, just a date or just a url.
* @param tags
* @throws IOException
* @throws InterruptedException
*/
public void fetchPosts( String tags, String date, String url )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
if ( tags != null )
{
formFields.put( "tag", tags );
}
if ( date != null )
{
formFields.put( "dt", date );
}
if ( url != null )
{
formFields.put( "url", url );
}
doService( "posts", "get", formFields );
}
/**
* Fetches a list of most recent posts, possibly filtered by tag, maxes out at 100.
*/
public void fetchRecentPosts( String tags, String count )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
if ( tags != null )
{
formFields.put( "tag", tags );
}
if ( count != null )
{
formFields.put( "count", count );
}
doService( "posts", "recent", formFields );
}
/**
* Fetches all posts. use sparingly. call update function first to see if you need to fetch this at all.
*/
public void fetchAllPosts( String tags )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
if ( tags != null )
{
formFields.put( "tag", tags );
}
doService( "posts", "all", formFields );
}
/**
* Gets the time of the last update.
*/
public void fetchUpdateTime()
throws IOException, InterruptedException
{
doService( "posts", "update", null );
}
/**
* Adds the post with the given information.
* @param url
* @param description
* @param tags
* @param extended
* @param replace
* @throws IOException
* @throws InterruptedException
*/
public void addPost( String url, String description, String tags, String extended, Boolean replace )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
formFields.put( "url", url );
formFields.put( "description", description );
formFields.put( "extended", extended );
formFields.put( "tags", tags );
// Todo
// formFields.put("dt", new SimpleDateFormat("yyyy-MM-ddTHH:mm:ssZ").format(new Date()));
formFields.put( "replace", replace.toString() );
doService( "posts", "add", formFields );
}
/**
* Deletes the post at the given url.
* @param url
* @throws IOException
* @throws InterruptedException
*/
public void deletePost( String url )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
formFields.put( "url", url );
doService( "posts", "delete", formFields );
}
/**
* Fetches the tags used by this account.
* @throws IOException
* @throws InterruptedException
*/
public void fetchTags()
throws IOException, InterruptedException
{
doService( "tags", "get", null );
}
/**
* Renames the given tag.
* @param url
* @throws IOException
* @throws InterruptedException
*/
public void renameTag( String oldName, String newName )
throws IOException, InterruptedException
{
HashMap formFields = new HashMap();
formFields.put( "old", oldName );
formFields.put( "new", newName );
doService( "tags", "rename", formFields );
}
/**
* Invokes the delicous service defined by the supplied url and query.
* The username and password are supplied for http-auth basic authorisation.
* According to the guidelines at http://del.icio.us/doc/api this method:
* <ul>
* <li>waits 1 second before properly executing</li>
* <li>bails out on receipt of a 503 error</li>
* </ul>
* For an ordinairy internal server error this method will try up to
* a maximum number of times before bailing out.
* @param category TODO
* @param command
* @param formFields
* @throws IOException
* @throws InterruptedException
*/
public void doService( String category, String command, HashMap formFields )
throws IOException, InterruptedException
{
int tryCount = 0;
boolean shouldTry = true;
while ( shouldTry )
{
tryCount++;
try
{
doServiceImpl( category, command, formFields );
shouldTry = false;
}
catch ( RuntimeException e )
{
if ( code == HttpStatus.SC_INTERNAL_SERVER_ERROR )
{
logger.warn("got an internal server error");
if ( tryCount == 3 )
{
logger.warn("giving up");
throw e;
}
logger.warn("will try again");
}
else
{
throw e;
}
}
}
}
private void doServiceImpl( String category, String command, HashMap formFields )
throws InterruptedException, IOException, HttpException
{
Thread.sleep( Messages.getCourtesyTime().longValue() );
HttpClient client = new HttpClient();
client.getState().setCredentials( new AuthScope( Messages.getDeliciousHost(), 80 ),
new UsernamePasswordCredentials( userName, password ) );
GetMethod httpMethod = new GetMethod( getServiceUrl( category, command ) );
if ( formFields != null && formFields.size() > 0 )
{
httpMethod.setQueryString( getQuery( formFields ) );
}
code = connection.executeMethod( client, httpMethod );
notifyListener( httpMethod, code );
httpMethod.releaseConnection();
if ( code != HttpStatus.SC_OK )
{
throw new RuntimeException( serviceUnavailableMessage );
}
}
private void notifyListener( GetMethod httpMethod, int code )
throws IOException
{
// all this to cope with the fact the response body may be null for the test connection
Reader reader;
InputStream responseBodyAsStream = httpMethod.getResponseBodyAsStream();
if ( responseBodyAsStream != null )
{
reader = new InputStreamReader( responseBodyAsStream );
}
else
{
reader = new StringReader( "" );
}
getListener().serviceDone( code, reader );
}
/**
* Gets the service url for the given name.
* @param category TODO
* @param command
* @return
*/
private String getServiceUrl( String category, String command )
{
return Messages.getDeliciousUrl() + "/" + category + "/" + command;
}
/**
* Converts the given hashmap of field key-value pairs into the equivalent array.
* @param formFields
* @return
*/
private NameValuePair[] getQuery( HashMap formFields )
{
NameValuePair[] query = new NameValuePair[formFields.size()];
int i = 0;
Iterator allFields = formFields.entrySet().iterator();
while ( allFields.hasNext() )
{
Map.Entry field = (Map.Entry) allFields.next();
query[i++] = new NameValuePair( (String) field.getKey(), (String) field.getValue() );
}
return query;
}
}