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; } }