Browse Source

2010-03-06 Tatsuhiro Tsujikawa <t-tujikawa@users.sourceforge.net>

	Added changeUri XML-RPC method.  This method removes/adds URIs
	dynamically.
	* doc/aria2c.1.txt
	* src/AbstractCommand.cc
	* src/DownloadContext.cc
	* src/DownloadContext.h
	* src/FileEntry.cc
	* src/FileEntry.h
	* src/Request.cc
	* src/Request.h
	* src/RequestGroup.cc
	* src/RequestGroupMan.cc
	* src/XmlRpcMethodFactory.cc
	* src/XmlRpcMethodImpl.cc
	* src/XmlRpcMethodImpl.h
	* test/FileEntryTest.cc
	* test/XmlRpcMethodTest.cc
Tatsuhiro Tsujikawa 15 năm trước cách đây
mục cha
commit
b1713e6373

+ 20 - 0
ChangeLog

@@ -1,3 +1,23 @@
+2010-03-06  Tatsuhiro Tsujikawa  <t-tujikawa@users.sourceforge.net>
+
+	Added changeUri XML-RPC method.  This method removes/adds URIs
+	dynamically.
+	* doc/aria2c.1.txt
+	* src/AbstractCommand.cc
+	* src/DownloadContext.cc
+	* src/DownloadContext.h
+	* src/FileEntry.cc
+	* src/FileEntry.h
+	* src/Request.cc
+	* src/Request.h
+	* src/RequestGroup.cc
+	* src/RequestGroupMan.cc
+	* src/XmlRpcMethodFactory.cc
+	* src/XmlRpcMethodImpl.cc
+	* src/XmlRpcMethodImpl.h
+	* test/FileEntryTest.cc
+	* test/XmlRpcMethodTest.cc
+
 2010-03-06  Tatsuhiro Tsujikawa  <t-tujikawa@users.sourceforge.net>
 
 	Rewritten copy ctor of RequestSlot to use initialization list.

+ 6 - 2
doc/aria2c.1

@@ -2,12 +2,12 @@
 .\"     Title: aria2c
 .\"    Author: [FIXME: author] [see http://docbook.sf.net/el/author]
 .\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
-.\"      Date: 02/25/2010
+.\"      Date: 03/06/2010
 .\"    Manual: Aria2 Manual
 .\"    Source: Aria2 1.9.0a
 .\"  Language: English
 .\"
-.TH "ARIA2C" "1" "02/25/2010" "Aria2 1\&.9\&.0a" "Aria2 Manual"
+.TH "ARIA2C" "1" "03/06/2010" "Aria2 1\&.9\&.0a" "Aria2 Manual"
 .\" -----------------------------------------------------------------
 .\" * Define some portability stuff
 .\" -----------------------------------------------------------------
@@ -2467,6 +2467,10 @@ This method changes the position of the download denoted by \fIgid\fR\&. \fIpos\
 .sp
 For example, if GID#1 is placed in position 3, aria2\&.changePosition(1, \-1, POS_CUR) will change its position to 2\&. Additional aria2\&.changePosition(1, 0, POS_SET) will change its position to 0(the beginning of the queue)\&.
 .sp
+\fBaria2\&.changeUri\fR \fIgid, fileIndex, delUris, addUris[, position]\fR
+.sp
+This method removes URIs in \fIdelUris\fR from and appends URIs in \fIaddUris\fR to download denoted by \fIgid\fR\&. \fIdelUris\fR and \fIaddUris\fR are list of string\&. A download can contain multiple files and URIs are attached to each file\&. \fIfileIndex\fR is used to select which file to remove/attach given URIs\&. \fIposition\fR is used to specify where URIs are inserted in the existing waiting URI list\&. \fIposition\fR is 0\-based\&. When \fIposition\fR is omitted, URIs are appended to the back of the list\&. This method first execute removal and then addition\&. \fIposition\fR is the position after URIs are removed, not the position when this method is called\&. When removing URI, if same URIs exist in download, only one of them is removed for each URI in \fIdelUris\fR\&. In other words, there are three URIs "http://example\&.org/aria2" and you want remove them all, you have to specify (at least) 3 "http://example\&.org/aria2" in \fIdelUris\fR\&. This method returns a list which contains 2 integers\&. The first integer is the number of URIs deleted\&. The second integer is the number of URIs added\&.
+.sp
 \fBaria2\&.getOption\fR \fIgid\fR
 .sp
 This method returns options of the download denoted by \fIgid\fR\&. The response is of type struct\&. Its key is the name of option\&. The value type is string\&.

+ 19 - 1
doc/aria2c.1.html

@@ -3176,6 +3176,24 @@ destination position.</p></div>
 -1, POS_CUR) will change its position to 2. Additional
 aria2.changePosition(1, 0, POS_SET) will change its position to 0(the
 beginning of the queue).</p></div>
+<div class="paragraph"><p><strong>aria2.changeUri</strong> <em>gid, fileIndex, delUris, addUris[, position]</em></p></div>
+<div class="paragraph"><p>This method removes URIs in <em>delUris</em> from and appends URIs in
+<em>addUris</em> to download denoted by <em>gid</em>. <em>delUris</em> and <em>addUris</em> are
+list of string. A download can contain multiple files and URIs are
+attached to each file.  <em>fileIndex</em> is used to select which file to
+remove/attach given URIs.  <em>position</em> is used to specify where URIs
+are inserted in the existing waiting URI list. <em>position</em> is
+0-based. When <em>position</em> is omitted, URIs are appended to the back of
+the list.  This method first execute removal and then
+addition. <em>position</em> is the position after URIs are removed, not the
+position when this method is called.  When removing URI, if same URIs
+exist in download, only one of them is removed for each URI in
+<em>delUris</em>. In other words, there are three URIs
+"http://example.org/aria2" and you want remove them all, you have to
+specify (at least) 3 "http://example.org/aria2" in <em>delUris</em>.  This
+method returns a list which contains 2 integers. The first integer is
+the number of URIs deleted. The second integer is the number of URIs
+added.</p></div>
 <div class="paragraph"><p><strong>aria2.getOption</strong> <em>gid</em></p></div>
 <div class="paragraph"><p>This method returns options of the download denoted by <em>gid</em>.  The
 response is of type struct. Its key is the name of option.  The value type
@@ -3722,7 +3740,7 @@ files in the program, then also delete it here.</p></div>
 <div id="footnotes"><hr /></div>
 <div id="footer">
 <div id="footer-text">
-Last updated 2010-02-23 23:01:01 JST
+Last updated 2010-03-06 23:18:27 JST
 </div>
 </div>
 </body>

+ 20 - 0
doc/aria2c.1.txt

@@ -1465,6 +1465,26 @@ For example, if GID#1 is placed in position 3, aria2.changePosition(1,
 aria2.changePosition(1, 0, POS_SET) will change its position to 0(the
 beginning of the queue).
 
+*aria2.changeUri* 'gid, fileIndex, delUris, addUris[, position]'
+
+This method removes URIs in 'delUris' from and appends URIs in
+'addUris' to download denoted by 'gid'. 'delUris' and 'addUris' are
+list of string. A download can contain multiple files and URIs are
+attached to each file.  'fileIndex' is used to select which file to
+remove/attach given URIs.  'position' is used to specify where URIs
+are inserted in the existing waiting URI list. 'position' is
+0-based. When 'position' is omitted, URIs are appended to the back of
+the list.  This method first execute removal and then
+addition. 'position' is the position after URIs are removed, not the
+position when this method is called.  When removing URI, if same URIs
+exist in download, only one of them is removed for each URI in
+'delUris'. In other words, there are three URIs
+"http://example.org/aria2" and you want remove them all, you have to
+specify (at least) 3 "http://example.org/aria2" in 'delUris'.  This
+method returns a list which contains 2 integers. The first integer is
+the number of URIs deleted. The second integer is the number of URIs
+added.
+
 *aria2.getOption* 'gid'
 
 This method returns options of the download denoted by 'gid'.  The

+ 8 - 0
src/AbstractCommand.cc

@@ -107,6 +107,14 @@ bool AbstractCommand::execute() {
       //logger->debug("CUID#%d - finished.", cuid);
       return true;
     }
+    if(!req.isNull() && req->removalRequested()) {
+      if(logger->debug()) {
+        logger->debug
+          ("CUID#%d - Discard original URI=%s because it is requested.",
+           cuid, req->getUrl().c_str());
+      }
+      return prepareForRetry(0);
+    }
     // TODO it is not needed to check other PeerStats every time.
     // Find faster Request when no segment is available.
     if(!req.isNull() && _fileEntry->countPooledRequest() > 0 &&

+ 4 - 3
src/DownloadContext.cc

@@ -39,6 +39,7 @@
 #include "FileEntry.h"
 #include "StringFormat.h"
 #include "util.h"
+#include "wallclock.h"
 
 namespace aria2 {
 
@@ -58,7 +59,7 @@ DownloadContext::DownloadContext(size_t pieceLength,
   _knowsTotalLength(true),
   _ownerRequestGroup(0),
   _downloadStartTime(0),
-  _downloadStopTime(_downloadStartTime)
+  _downloadStopTime(0)
 {
   SharedHandle<FileEntry> fileEntry
     (new FileEntry(util::escapePath(path), totalLength, 0));
@@ -67,12 +68,12 @@ DownloadContext::DownloadContext(size_t pieceLength,
 
 void DownloadContext::resetDownloadStartTime()
 {
-  _downloadStartTime.reset();
+  _downloadStartTime = global::wallclock;
 }
 
 void DownloadContext::resetDownloadStopTime()
 {
-  _downloadStopTime.reset();
+  _downloadStopTime = global::wallclock;
 }
 
 int64_t DownloadContext::calculateSessionTime() const

+ 5 - 0
src/DownloadContext.h

@@ -233,6 +233,11 @@ public:
 
   void resetDownloadStopTime();
 
+  const Time& getDownloadStopTime() const
+  {
+    return _downloadStopTime;
+  }
+
   int64_t calculateSessionTime() const;
   
   // Returns FileEntry at given offset. SharedHandle<FileEntry>() is

+ 48 - 1
src/FileEntry.cc

@@ -218,7 +218,9 @@ void FileEntry::storePool(const SharedHandle<Request>& request)
 void FileEntry::poolRequest(const SharedHandle<Request>& request)
 {
   removeRequest(request);
-  storePool(request);
+  if(!request->removalRequested()) {
+    storePool(request);
+  }
 }
 
 bool FileEntry::removeRequest(const SharedHandle<Request>& request)
@@ -330,4 +332,49 @@ void FileEntry::releaseRuntimeResource()
   _uriResults.clear();
 }
 
+template<typename InputIterator, typename T>
+static InputIterator findRequestByUri
+(InputIterator first, InputIterator last, const T& uri)
+{
+  for(; first != last; ++first) {
+    if(!(*first)->removalRequested() && (*first)->getUrl() == uri) {
+      return first;
+    }
+  }
+  return last;
+}
+
+bool FileEntry::removeUri(const std::string& uri)
+{
+  std::deque<std::string>::iterator itr =
+    std::find(_spentUris.begin(), _spentUris.end(), uri);
+  if(itr == _spentUris.end()) {
+    itr = std::find(_uris.begin(), _uris.end(), uri);
+    if(itr == _uris.end()) {
+      return false;
+    } else {
+      _uris.erase(itr);
+      return true;
+    }
+  } else {
+    _spentUris.erase(itr);
+    SharedHandle<Request> req;
+    std::deque<SharedHandle<Request> >::iterator riter =
+      findRequestByUri(_inFlightRequests.begin(), _inFlightRequests.end(), uri);
+    if(riter == _inFlightRequests.end()) {
+      riter = findRequestByUri(_requestPool.begin(), _requestPool.end(), uri);
+      if(riter == _requestPool.end()) {
+        return true;
+      } else {
+        req = *riter;
+        _requestPool.erase(riter);
+      }
+    } else {
+      req = *riter;
+    }
+    req->requestRemoval();
+    return true;
+  }
+}
+
 } // namespace aria2

+ 34 - 4
src/FileEntry.h

@@ -129,15 +129,43 @@ public:
     return _spentUris;
   }
 
-  void setUris(const std::vector<std::string>& uris)
+  size_t setUris(const std::vector<std::string>& uris)
   {
-    _uris = std::deque<std::string>(uris.begin(), uris.end());
+    _uris.clear();
+    return addUris(uris.begin(), uris.end());
   }
 
   template<typename InputIterator>
-  void addUris(InputIterator first, InputIterator last)
+  size_t addUris(InputIterator first, InputIterator last)
   {
-    _uris.insert(_uris.end(), first, last);
+    size_t count = 0;
+    for(; first != last; ++first) {
+      if(addUri(*first)) {
+        ++count;
+      }
+    }
+    return count;
+  }
+
+  bool addUri(const std::string& uri)
+  {
+    if(Request().setUrl(uri)) {
+      _uris.push_back(uri);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  bool insertUri(const std::string& uri, size_t pos)
+  {
+    if(Request().setUrl(uri)) {
+      pos = std::min(pos, _uris.size());
+      _uris.insert(_uris.begin()+pos, uri);
+      return true;
+    } else {
+      return false;
+    }
   }
 
   // Inserts _uris and _spentUris into uris.
@@ -234,6 +262,8 @@ public:
   {
     return _originalName;
   }
+
+  bool removeUri(const std::string& uri);
 };
 
 // Returns the first FileEntry which isRequested() method returns

+ 2 - 1
src/Request.cc

@@ -63,7 +63,8 @@ Request::Request():
   _maxPipelinedRequest(1),
   _method(METHOD_GET),
   _hasPassword(false),
-  _ipv6LiteralAddress(false)
+  _ipv6LiteralAddress(false),
+  _removalRequested(false)
 {}
 
 static std::string removeFragment(const std::string& url)

+ 12 - 0
src/Request.h

@@ -88,6 +88,8 @@ private:
 
   SharedHandle<PeerStat> _peerStat;
 
+  bool _removalRequested;
+
   bool parseUrl(const std::string& url);
 public:
   Request();
@@ -204,6 +206,16 @@ public:
 
   const SharedHandle<PeerStat>& initPeerStat();
 
+  void requestRemoval()
+  {
+    _removalRequested = true;
+  }
+
+  bool removalRequested() const
+  {
+    return _removalRequested;
+  }
+
   static const std::string METHOD_GET;
   static const std::string METHOD_HEAD;
 

+ 3 - 0
src/RequestGroup.cc

@@ -991,6 +991,9 @@ bool RequestGroup::needsFileAllocation() const
 
 DownloadResultHandle RequestGroup::createDownloadResult() const
 {
+  if(_logger->debug()) {
+    _logger->debug("GID#%d - Creating DownloadResult.", _gid);
+  }
   uint64_t sessionDownloadLength = 0;
 
 #ifdef ENABLE_BITTORRENT

+ 10 - 0
src/RequestGroupMan.cc

@@ -336,6 +336,16 @@ public:
   void operator()(const SharedHandle<RequestGroup>& group)
   {
     if(group->getNumCommand() == 0) {
+      SharedHandle<DownloadContext> dctx = group->getDownloadContext();
+      // DownloadContext::resetDownloadStopTime() is only called when
+      // download completed. If
+      // DownloadContext::getDownloadStopTime().isZero() is true, then
+      // there is a possibility that the download is error or
+      // in-progress and resetDownloadStopTime() is not called. So
+      // call it here.
+      if(dctx->getDownloadStopTime().isZero()) {
+        dctx->resetDownloadStopTime();
+      }
       try {
         group->closeFile();
         if(group->downloadFinished()) {

+ 2 - 0
src/XmlRpcMethodFactory.cc

@@ -79,6 +79,8 @@ XmlRpcMethodFactory::create(const std::string& methodName)
     return SharedHandle<XmlRpcMethod>(new TellStoppedXmlRpcMethod());
   } else if(methodName == GetOptionXmlRpcMethod::getMethodName()) {
     return SharedHandle<XmlRpcMethod>(new GetOptionXmlRpcMethod());
+  } else if(methodName == ChangeUriXmlRpcMethod::getMethodName()) {
+    return SharedHandle<XmlRpcMethod>(new ChangeUriXmlRpcMethod());
   } else if(methodName == ChangeOptionXmlRpcMethod::getMethodName()) {
     return SharedHandle<XmlRpcMethod>(new ChangeOptionXmlRpcMethod());
   } else if(methodName == GetGlobalOptionXmlRpcMethod::getMethodName()) {

+ 61 - 0
src/XmlRpcMethodImpl.cc

@@ -851,6 +851,67 @@ BDE GetSessionInfoXmlRpcMethod::process
   return result;
 }
 
+BDE ChangeUriXmlRpcMethod::process
+(const XmlRpcRequest& req, DownloadEngine* e)
+{
+  const BDE& params = req._params;
+  assert(params.isList());
+
+  if(params.size() < 4 ||
+     !params[0].isString() || !params[1].isInteger() ||
+     !params[2].isList() || !params[3].isList() ||
+     params[1].i() < 0) {
+    throw DL_ABORT_EX("Bad request");
+  }
+  size_t pos = 0;
+  bool posGiven = false;
+  getPosParam(params, 4, posGiven, pos);
+
+  int32_t gid = util::parseInt(params[0].s());
+  size_t index = params[1].i();
+  const BDE& deluris = params[2];
+  const BDE& adduris = params[3];
+  SharedHandle<RequestGroup> group = findRequestGroup(e->_requestGroupMan, gid);
+  if(group.isNull()) {
+    throw DL_ABORT_EX
+      (StringFormat("Cannot remove URIs from GID#%d", gid).str());
+  }
+  SharedHandle<DownloadContext> dctx = group->getDownloadContext();
+  const std::vector<SharedHandle<FileEntry> >& files = dctx->getFileEntries();
+  if(files.size() <= index) {
+    throw DL_ABORT_EX(StringFormat("fileIndex is out of range").str());
+  }
+  SharedHandle<FileEntry> s = files[index];
+  size_t delcount = 0;
+  for(BDE::List::const_iterator i = deluris.listBegin(),
+        eoi = deluris.listEnd(); i != eoi; ++i) {
+    if(s->removeUri((*i).s())) {
+      ++delcount;
+    }
+  }
+  size_t addcount = 0;
+  if(posGiven) {
+    for(BDE::List::const_iterator i = adduris.listBegin(),
+          eoi = adduris.listEnd(); i != eoi; ++i) {
+      if(s->insertUri((*i).s(), pos)) {
+        ++addcount;
+        ++pos;
+      }
+    }
+  } else {
+    for(BDE::List::const_iterator i = adduris.listBegin(),
+          eoi = adduris.listEnd(); i != eoi; ++i) {
+      if(s->addUri((*i).s())) {
+        ++addcount;
+      }
+    }
+  }
+  BDE res = BDE::list();
+  res << delcount;
+  res << addcount;
+  return res;
+}
+
 BDE SystemMulticallXmlRpcMethod::process
 (const XmlRpcRequest& req, DownloadEngine* e)
 {

+ 11 - 0
src/XmlRpcMethodImpl.h

@@ -341,6 +341,17 @@ public:
   }
 };
 
+class ChangeUriXmlRpcMethod:public XmlRpcMethod {
+protected:
+  virtual BDE process(const XmlRpcRequest& req, DownloadEngine* e);
+public:
+  static const std::string& getMethodName()
+  {
+    static std::string methodName = "aria2.changeUri";
+    return methodName;
+  }
+};
+
 class GetSessionInfoXmlRpcMethod:public XmlRpcMethod {
 protected:
   virtual BDE process(const XmlRpcRequest& req, DownloadEngine* e);

+ 67 - 0
test/FileEntryTest.cc

@@ -15,6 +15,10 @@ class FileEntryTest : public CppUnit::TestFixture {
   CPPUNIT_TEST(testGetRequest);
   CPPUNIT_TEST(testGetRequest_disableSingleHostMultiConnection);
   CPPUNIT_TEST(testReuseUri);
+  CPPUNIT_TEST(testAddUri);
+  CPPUNIT_TEST(testAddUris);
+  CPPUNIT_TEST(testInsertUri);
+  CPPUNIT_TEST(testRemoveUri);
   CPPUNIT_TEST_SUITE_END();
 public:
   void setUp() {}
@@ -25,6 +29,10 @@ public:
   void testGetRequest();
   void testGetRequest_disableSingleHostMultiConnection();
   void testReuseUri();
+  void testAddUri();
+  void testAddUris();
+  void testInsertUri();
+  void testRemoveUri();
 };
 
 
@@ -149,4 +157,63 @@ void FileEntryTest::testReuseUri()
   CPPUNIT_ASSERT_EQUAL(std::string("ftp://localhost/aria2.zip"), uris[2]);
 }
 
+void FileEntryTest::testAddUri()
+{
+  FileEntry file;
+  CPPUNIT_ASSERT(file.addUri("http://good"));
+  CPPUNIT_ASSERT(!file.addUri("bad"));
+}
+
+void FileEntryTest::testAddUris()
+{
+  FileEntry file;
+  std::string uris[] = {"bad", "http://good"};
+  CPPUNIT_ASSERT_EQUAL((size_t)1, file.addUris(&uris[0], &uris[2]));
+}
+
+void FileEntryTest::testInsertUri()
+{
+  FileEntry file;
+  CPPUNIT_ASSERT(file.insertUri("http://example.org/1", 0));
+  CPPUNIT_ASSERT(file.insertUri("http://example.org/2", 0));
+  CPPUNIT_ASSERT(file.insertUri("http://example.org/3", 1));
+  CPPUNIT_ASSERT(file.insertUri("http://example.org/4", 5));
+  std::deque<std::string> uris = file.getRemainingUris();
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/2"), uris[0]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/3"), uris[1]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/1"), uris[2]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/4"), uris[3]);
+}
+
+void FileEntryTest::testRemoveUri()
+{
+  SharedHandle<InOrderURISelector> selector(new InOrderURISelector());
+  FileEntry file;
+  file.addUri("http://example.org/");
+  CPPUNIT_ASSERT(file.removeUri("http://example.org/"));
+  CPPUNIT_ASSERT(file.getRemainingUris().empty());
+  CPPUNIT_ASSERT(!file.removeUri("http://example.org/"));
+
+  file.addUri("http://example.org/");
+  SharedHandle<Request> exampleOrgReq = file.getRequest(selector);
+  CPPUNIT_ASSERT(!exampleOrgReq->removalRequested());
+  CPPUNIT_ASSERT_EQUAL((size_t)1, file.getSpentUris().size());
+  CPPUNIT_ASSERT(file.removeUri("http://example.org/"));
+  CPPUNIT_ASSERT(file.getSpentUris().empty());
+  CPPUNIT_ASSERT(exampleOrgReq->removalRequested());
+  file.poolRequest(exampleOrgReq);
+  CPPUNIT_ASSERT_EQUAL((size_t)0, file.countPooledRequest());
+
+  file.addUri("http://example.org/");
+  exampleOrgReq = file.getRequest(selector);
+  file.poolRequest(exampleOrgReq);
+  CPPUNIT_ASSERT_EQUAL((size_t)1, file.countPooledRequest());
+  CPPUNIT_ASSERT(file.removeUri("http://example.org/"));
+  CPPUNIT_ASSERT_EQUAL((size_t)0, file.countPooledRequest());
+  CPPUNIT_ASSERT(file.getSpentUris().empty());
+
+  file.addUri("http://example.org/");
+  CPPUNIT_ASSERT(!file.removeUri("http://example.net"));
+}
+
 } // namespace aria2

+ 129 - 0
test/XmlRpcMethodTest.cc

@@ -75,6 +75,8 @@ class XmlRpcMethodTest:public CppUnit::TestFixture {
   CPPUNIT_TEST(testChangePosition);
   CPPUNIT_TEST(testChangePosition_fail);
   CPPUNIT_TEST(testGetSessionInfo);
+  CPPUNIT_TEST(testChangeUri);
+  CPPUNIT_TEST(testChangeUri_fail);
   CPPUNIT_TEST(testSystemMulticall);
   CPPUNIT_TEST(testSystemMulticall_fail);
   CPPUNIT_TEST_SUITE_END();
@@ -135,6 +137,8 @@ public:
   void testChangePosition();
   void testChangePosition_fail();
   void testGetSessionInfo();
+  void testChangeUri();
+  void testChangeUri_fail();
   void testSystemMulticall();
   void testSystemMulticall_fail();
 };
@@ -779,6 +783,131 @@ void XmlRpcMethodTest::testChangePosition_fail()
   CPPUNIT_ASSERT_EQUAL(1, res._code);
 }
 
+void XmlRpcMethodTest::testChangeUri()
+{
+  SharedHandle<FileEntry> files[3];
+  for(int i = 0; i < 3; ++i) {
+    files[i].reset(new FileEntry());
+  }
+  files[1]->addUri("http://example.org/aria2.tar.bz2");
+  files[1]->addUri("http://example.org/mustremove1");
+  files[1]->addUri("http://example.org/mustremove2");
+  SharedHandle<DownloadContext> dctx(new DownloadContext());
+  dctx->setFileEntries(&files[0], &files[3]);
+  SharedHandle<RequestGroup> group(new RequestGroup(_option));
+  group->setDownloadContext(dctx);
+  _e->_requestGroupMan->addReservedGroup(group);
+
+  ChangeUriXmlRpcMethod m;
+  XmlRpcRequest req(ChangeUriXmlRpcMethod::getMethodName(), BDE::list());
+  req._params << std::string("1"); // GID
+  req._params << 1; // index of FileEntry
+  BDE removeuris = BDE::list();
+  removeuris << std::string("http://example.org/mustremove1");
+  removeuris << std::string("http://example.org/mustremove2");
+  removeuris << std::string("http://example.org/notexist");
+  req._params << removeuris;
+  BDE adduris = BDE::list();
+  adduris << std::string("http://example.org/added1");
+  adduris << std::string("http://example.org/added2");
+  adduris << std::string("baduri");
+  adduris << std::string("http://example.org/added3");
+  req._params << adduris;
+  XmlRpcResponse res = m.execute(req, _e.get());
+  CPPUNIT_ASSERT_EQUAL(0, res._code);
+  CPPUNIT_ASSERT_EQUAL((int64_t)2, res._param[0].i());
+  CPPUNIT_ASSERT_EQUAL((int64_t)3, res._param[1].i());
+  CPPUNIT_ASSERT_EQUAL((size_t)0, files[0]->getRemainingUris().size());
+  CPPUNIT_ASSERT_EQUAL((size_t)0, files[2]->getRemainingUris().size());
+  std::deque<std::string> uris = files[1]->getRemainingUris();
+  CPPUNIT_ASSERT_EQUAL((size_t)4, uris.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/aria2.tar.bz2"),uris[0]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added1"), uris[1]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added2"), uris[2]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added3"), uris[3]);
+
+  // Change adduris
+  adduris = BDE::list();
+  adduris << std::string("http://example.org/added1-1");
+  adduris << std::string("http://example.org/added1-2");
+  req._params[3] = adduris;
+  // Set position parameter
+  req._params << 2;
+  res = m.execute(req, _e.get());
+  CPPUNIT_ASSERT_EQUAL(0, res._code);
+  CPPUNIT_ASSERT_EQUAL((int64_t)0, res._param[0].i());
+  CPPUNIT_ASSERT_EQUAL((int64_t)2, res._param[1].i());
+  uris = files[1]->getRemainingUris();
+  CPPUNIT_ASSERT_EQUAL((size_t)6, uris.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added1-1"), uris[2]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added1-2"), uris[3]);
+
+  // Change index of FileEntry
+  req._params[1] = 0;
+  // Set position far beyond the size of uris in FileEntry.
+  req._params[4] = 1000;
+  res = m.execute(req, _e.get());
+  CPPUNIT_ASSERT_EQUAL(0, res._code);
+  CPPUNIT_ASSERT_EQUAL((int64_t)0, res._param[0].i());
+  CPPUNIT_ASSERT_EQUAL((int64_t)2, res._param[1].i());
+  uris = files[0]->getRemainingUris();
+  CPPUNIT_ASSERT_EQUAL((size_t)2, uris.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added1-1"), uris[0]);
+  CPPUNIT_ASSERT_EQUAL(std::string("http://example.org/added1-2"), uris[1]);
+}
+
+void XmlRpcMethodTest::testChangeUri_fail()
+{
+  SharedHandle<FileEntry> files[3];
+  for(int i = 0; i < 3; ++i) {
+    files[i].reset(new FileEntry());
+  }
+  SharedHandle<DownloadContext> dctx(new DownloadContext());
+  dctx->setFileEntries(&files[0], &files[3]);
+  SharedHandle<RequestGroup> group(new RequestGroup(_option));
+  group->setDownloadContext(dctx);
+  _e->_requestGroupMan->addReservedGroup(group);
+
+  ChangeUriXmlRpcMethod m;
+  XmlRpcRequest req(ChangeUriXmlRpcMethod::getMethodName(), BDE::list());
+  req._params << std::string("1"); // GID
+  req._params << 0; // index of FileEntry
+  BDE removeuris = BDE::list();
+  req._params << removeuris;
+  BDE adduris = BDE::list();
+  req._params << adduris;
+  XmlRpcResponse res = m.execute(req, _e.get());
+  CPPUNIT_ASSERT_EQUAL(0, res._code);
+
+  req._params[0] = std::string("2");
+  res = m.execute(req, _e.get());  
+  // RPC request fails because GID#2 does not exist.
+  CPPUNIT_ASSERT_EQUAL(1, res._code);
+
+  req._params[0] = std::string("1");
+  req._params[1] = 3;
+  res = m.execute(req, _e.get());  
+  // RPC request fails because FileEntry#3 does not exist.
+  CPPUNIT_ASSERT_EQUAL(1, res._code);
+
+  req._params[1] = std::string("0");
+  res = m.execute(req, _e.get());  
+  // RPC request fails because index of FileEntry is string.
+  CPPUNIT_ASSERT_EQUAL(1, res._code);
+
+  req._params[1] = 0;
+  req._params[2] = std::string("http://url");
+  res = m.execute(req, _e.get());  
+  // RPC request fails because 3rd param is not list.
+  CPPUNIT_ASSERT_EQUAL(1, res._code);
+
+  req._params[2] = BDE::list();
+  req._params[3] = std::string("http://url");
+  res = m.execute(req, _e.get());  
+  // RPC request fails because 4th param is not list.
+  CPPUNIT_ASSERT_EQUAL(1, res._code);
+}
+
 void XmlRpcMethodTest::testGetSessionInfo()
 {
   GetSessionInfoXmlRpcMethod m;