Sunday, October 16, 2011

Document Management System with CouchDB - Second Part

In the first part we installed CouchDB and explain how to create databases, documents, update them, create attachments and run queries.

Now we will design a DMS and plan for the different functionality we will need with one example.

For simplicity we will say our documents will be imported in batch for which it makes sense to have a convention for the file names client_cat_subcat_year_month_investor.ext. After importing the below 14 documents we can start navigating the tree. Note that in this example the importer code will replace the month by two digits so "3" becomes "03"
1_1_1_2003_1_1.pdf
1_1_1_2003_1_2.pdf
1_1_1_2003_2_3.pdf
1_1_1_2003_2_4.pdf
1_1_1_2004_3_5.pdf
1_1_2_2005_4_6.pdf
1_1_3_2006_5_7.pdf
1_1_3_2006_6_8.pdf
1_2_4_2007_7_9.pdf
2_3_5_2008_8_10.pdf
2_3_5_2009_9_11.pdf
2_3_6_2010_10_12.pdf
2_3_6_2010_11_13.pdf
2_3_7_2011_12_14.pdf

We want to offer a tree view of our documents. First we show the available clients. When the user clicks one of them we show the categories available. Clicking one of the categories will render all subcategories and so on. Let us go by the example of the first document (1_1_1_2003_1_1.pdf)

Here is how to pull all clients. Note the use of {} which means "any":
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=1" -G --data-urlencode startkey='[1]' --data-urlencode endkey='[{}]'
{"rows":[
{"key":[1],"value":9},
{"key":[2],"value":5}
]}
The categories for a client (1)
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=2" -G --data-urlencode startkey='[1]' --data-urlencode endkey='[1, {}]'
{"rows":[
{"key":[1,1],"value":8},
{"key":[1,2],"value":1}
]}
The subcategories for a client category (1,1)
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=3" -G --data-urlencode startkey='[1,1]' --data-urlencode endkey='[1, 1, {}]'
{"rows":[
{"key":[1,1,1],"value":5},
{"key":[1,1,2],"value":1},
{"key":[1,1,3],"value":2}
]} 
The effective years for the client category subcategory (1,1,1)
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=4" -G --data-urlencode startkey='[1,1,1]' --data-urlencode endkey='[1,1,1,{}]'
{"rows":[
{"key":[1,1,1,"2003"],"value":4},
{"key":[1,1,1,"2004"],"value":1}
]}
The effective months for the client category subcategory year (1,1,1,"2003"). Note the quotes for 2003 as it is a String obtained from a token.
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=5" -G --data-urlencode startkey='[1,1,1,"2003"]' --data-urlencode endkey='[1,1,1,"2003",{}]'
{"rows":[
{"key":[1,1,1,"2003","01"],"value":2},
{"key":[1,1,1,"2003","02"],"value":2}
]}
The documents for the client category subcategory year month(1,1,1,"2003","01"). Note "01" instead "1" just because our importer is treating months as 2 digits values.
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=6" -G --data-urlencode startkey='[1,1,1,"2003","01"]' --data-urlencode endkey='[1,1,1,"2003","01",{}]'
{"rows":[
{"key":[1,1,1,"2003","01","1_1_1_2003_1_1.pdf"],"value":1},
{"key":[1,1,1,"2003","01","1_1_1_2003_1_2.pdf"],"value":1}
]}
So you have figured if we want to navigate to document 2_3_5_2009_9_11.pdf we just have to pass startkey='[2]' and endkey=[2,3,5,"2009","09",{}] and the group_level 6:
nestor-nu:~ nestor$ curl -X GET "http://127.0.0.1:5984/dms4/_design/Document/_view/tree?group=true&group_level=6" -G --data-urlencode startkey='[2,3,5,"2009","09"]' --data-urlencode endkey='[2,3,5,"2009","09",{}]'
{"rows":[
{"key":[2,3,5,"2009","09","2_3_5_2009_9_11.pdf"],"value":1}
]}

If we were to build a JSON web service we just need to accept the startkey. The endkey is always an array containing startkey with a new last element: {}.

Specifically if I use BHUB (which is just a concept around Spring Framework) a typical request and response will look like:
http://localhost:8080/nu-app/dms/document/tree?root=2,3,5,"2009","09"&ert=json
{"rows":[
{"key":[2,3,5,"2009","09","2_3_5_2009_9_11.pdf"],"value":1}
]}

In the final part I show how to use Java Erktop library to implement the DMS we have been covering so far.

Document Management System with CouchDB - First Part

I will start documenting about my experience using CouchDB to build a Document Management System (DMS), an important component of any Content Management System (CMS).

The first part concentrates on installing and using CouchDB in OSX and Ubuntu.

OSX

Alternatively you could install from sources which I prefer to get later and greatest.

  1. Download any pending updates for OSX. Then latest version of XCode
  2. Install homebrew if you still not have it. It is the best package manager for OSX.
  3. /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/gist/323731)"
    
  4. Install couchDB. Following instructions from http://wiki.apache.org/couchdb/Installation with just one command. It could take a while, if it hangs then restart again, it will continue from where it broke.
  5. brew install couchdb
    
  6. Start the server.
    $ couchdb
    $ curl -X GET http://127.0.0.1:5984/
    
  7. If you need to change the default port:
    sudo vi  /usr/local/etc/couchdb/default.ini
    

Ubuntu

In Ubuntu I decided to build from sources to get latest available version for my 10.10 Maverick. For 11.4 I found this also works but you have to issue 'sudo apt-get remove libmozjs185-dev' in order to build.

Using CouchDB

Let us start interacting with CouchDB to create a database, a document, attach a file to it, update it etc. We use curl to be sure we can issue different HTTP request method (GET, POST, PUT, DELETE).
  1. We create our Document Management System database and we confirm it was created:
    $ curl -X PUT http://127.0.0.1:5984/dms
    {"ok":true}
    $ curl -X GET http://127.0.0.1:5984/_all_dbs
    ["_replicator","_users","dms"]
    
  2. Let us create a first document. In this example we use POST instead of PUT so we get a UUID generated by CouchDB:
    $ curl -d '{
        "name":"Investor Document 11",
        "clientId": "1001",
        "createdByEmployeeId": "2",
        "reviewedByEmployeeId": "1",
        "approvedByManagerId": "21",
        "created": "2/2/2011",
        "reviewed": "2/3/2011",
        "approved": "2/4/2011",
        "investorId": "32",
        "categoryId": "2",
        "statusId": "2"
    }' -H "Content-Type: application/json" -X POST http://127.0.0.1:5984/dms
    
    Result:
    {"ok":true,"id":"296ef7cde8fe533efe0c7dded873505b","rev":"1-d5dd0fa82df07553f3a2b82947864fc6"}
    
  3. Let us attach a file to the above document. Note we need the document is and rev:
    $ curl -X PUT -H "Content-Type: application/pdf"  --data-binary @DailyReport.pdf  $DMS/296ef7cde8fe533efe0c7dded873505b/DailyReport.pdf?rev=1-d5dd0fa82df07553f3a2b82947864fc6
    
  4. You can visually manage your CouchDB server via Futon user interface. Just hit http://localhost:5984/_utils/ and start playing with it.
  5. Create some documents for different combinations of categoryId, clientId and investorId either from curl or from Futon.
  6. Let us start querying our DB.

    You query a View in CouchDB. Views are a combination of two functions that are applied to the original data: Map and Reduce (MapReduce style: Map functions generate indexes and Reduce queries are requests against them). Map function as its name suggests specifies the mapping between the document structure and the structure of the View. Reduce function as its name suggests specifies how to group the resulting data to reduce the results. The View is consequently just a transformation of the document where an index is usually defined. If you need to group a Reduce step will be applied as well.

    You must become familiar with how to write the map and reduce functions for Views. This is done using the javascript language. From Futon select "Temporary View ..." option from the View dropdown. You have two panes now, the left is for your Map function and the right is for the Reduce. By default you see CouchDB proposes the below code which is equivalent to "Do not use any custom key and show all values from the document". There is no transformation nor custom index at all in this case, however if no key is specified couchDB uses the document id as unique identifier). Remember the Map function generates rows containing the id, an optional key and an optional value.
    function(doc) {
      emit(null, doc);
    }
    
  7. Let us edit the function to "Use the name as key and show only clientId and investorId". When you run the view using both functions you will realize the difference. By now you should be aware that emit() just accepts two parameters, the key for an index and the value that will be returned. Of course the results come ordered by the Key if provided. Both key and value are json expressions as well.
    function(doc) {
      //emit(doc.name, doc);
      if(doc.name && doc.clientId) {
        var key = doc.name;
        var value = {name: doc.name, clientId: doc.clientId, investorId: doc.investorId}
        emit(key, value);
      }
    }
    
  8. Here we use a composite key out of the clientId and the investorId so we can find the documents for that combination. Again the results are ordered first by clientId and later by investorId:
    function(doc) {
      if(doc.clientId && doc.investorId) {
        var key = [doc.clientId, doc.investorId];
        var value = {name: doc.name, clientId: doc.clientId, investorId: doc.investorId}
        emit(key, value);
      }
    }
    
  9. Save the view. The options you pick will be used in the URL to retrieve the View results. I have decided to use "common" for the design document name and "by_client_investor" for the name of the view:
    Design Document: _design/common
    View Name: by_client_investor
    

  10. Now the View is saved so we can query it at any time. The View is now "Permanent" and not longer "Temporary". Let us query it for just one key. Note that as we decided to use an array as key we will need to look for something like: ["1000","30"]. As you might have notice the key contains characters that must be URL encoded, in this case %5B%221000%22%2C%2230%22%5D. Here is how the command will look like:
    $ curl -X GET http://127.0.0.1:5984/dms/_design/common/_view/by_client_investor?key=%5B%221000%22%2C%2230%22%5D
    
    Alternatively you can use a more clear approach using some other curl flags:
    curl -X GET http://127.0.0.1:5984/dms/_design/common/_view/by_client_investor -G --data-urlencode key='["1000","30"]'
    
  11. Here is how you use curl to create and execute a temporary View from the command line. Here we are using categoryId as a key and getting the whole document as a result of the "non existent" transformation.
    curl -X POST http://127.0.0.1:5984/dms/_temp_view -H "Content-Type: application/json" -d \
    '{
      "map": "function(doc) {
                if (doc.categoryId) {
                  emit(doc.categoryId, doc);
                }
              }"
    }'
    
  12. Let us explore the results of the below temporary View. Here we are insterested in the total documents by category. As we are grouping we need to use a Reduce function where we take advantage of the provided _count. Note the key is null because it counts all of the existing documents.
    curl -X POST http://127.0.0.1:5984/dms/_temp_view -H "Content-Type: application/json" -d \
    '{
      "map": "function(doc) {
                if (doc.categoryId) {
                  emit(doc.categoryId, doc);
                }
              }",
      "reduce": "_count"
    }'
    
  13. Here is how we generate the counting by key which translates to use "Grouping=exact" from Futon or as shown below "group=true" from the HTTP request:
    curl -X POST http://127.0.0.1:5984/dms/_temp_view?group=true -H "Content-Type: application/json" -d \
    '{
      "map": "function(doc) {
                if (doc.categoryId) {
                  emit(doc.categoryId, doc);
                }
              }",
      "reduce": "_count"
    }'
    
  14. We already saw how to make a View permanent while saving it from Futon. Here is how from an HTTP request you do the same. This time we are adding a View called category_count to a Design Document called category:
    curl -X PUT http://127.0.0.1:5984/dms/_design/category -d \
    '{
       "_id": "_design/category",
       "language": "javascript",
       "views": {
         "count": {
           "map":
             "function(doc) {
               if (doc.categoryId) {
                 emit(doc.categoryId, doc);
               }
             }",
           "reduce": "_count"
          }
       }
    }'
    
  15. As we already saw we can query this view like this:
    curl -X GET http://127.0.0.1:5984/dms/_design/category/_view/count
    {"rows":[
    {"key":"1","value":17},
    {"key":"2","value":1}
    ]}
    

Some other examples

Here is how you would pull document information, delete its attachment (named the same as the document) using the revision number, try to delete it again and get an error, try to pull the attachment from the document and get an error and pull information again to confirm the document is still in the DB but simply it does not have any attachments. Note that I am accessing here now a production system where we use SSL with user and password:
$ curl -k -X GET "https://user:password@example.com:6984/dms/sample.pdf"
{"_id":"sample.pdf","_rev":"1-adb7b6f2e32d73758dfa16966c1caef9","approvedOn":"2012-03-15T16:34:08.000-0400","createdByEmployeeEmail":"nestor@example.com","title":"sample.pdf","_attachments":{"sample.pdf":{"content_type":"application/pdf","revpos":1,"digest":"md5-ZRJB3hYW9LuwL2p9wjJr0g==","length":167546,"stub":true}}}
$ curl -k -X DELETE "https://user:password@example.com:6984/dms/sample.pdf/sample.pdf/?rev=1-adb7b6f2e32d73758dfa16966c1caef9"
{"ok":true,"id":"sample.pdf","rev":"2-87988b99af60e2f7cb9022b65b7565d5"}
$ curl -k -X DELETE "https://user:password@example.com:6984/dms/sample.pdf/sample.pdf/?rev=1-adb7b6f2e32d73758dfa16966c1caef9"
{"error":"conflict","reason":"Document update conflict."}
$ curl -k -X GET "https://user:password@example.com:6984/dms/sample.pdf/sample.pdf"
{"error":"not_found","reason":"Document is missing attachment"}
$ curl -k -X GET "https://user:password@example.com:6984/dms/sample.pdf"
{"_id":"sample.pdf","_rev":"1-adb7b6f2e32d73758dfa16966c1caef9","approvedOn":"2012-03-15T16:34:08.000-0400","createdByEmployeeEmail":"nestor@example.com","title":"sample.pdf"}

Logging

If you are unsure where couchdb is logging just issue the below command:
$ curl -X GET http://localhost:5984/_config/log {"file":"/usr/local/var/log/couchdb/couch.log","include_sasl":"true","level":"info"}
You can also get the latest log lines directly from the below request:
$ curl -X GET http://localhost:5984/_log

Review

At this point you can interact with CouchDB from any language using plain REST commands. You might want to use some abstractions with an API that allows you to go through CRUD operations with CouchDB without being concern about the details of sending and parsing JSON.

In the next part we start the design of the DMS for which we will not use any specific language other than plain HTTP with the help of curl.

Monday, October 03, 2011

Tomcat 7 scans all jars for TLDs

Tomcat 7 scans all jars for TLDs. I am unsure if tomcat 6 does the same:
INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.

Once the log level was increased to FINE in conf/logging.properties:
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = FINE

We got:
...
FINE: No TLD files were found in [file:/opt/tomcat/webapps/nestorurquiza-app/WEB-INF/lib/org.springframework.transaction-3.0.5.RELEASE.jar]. Consider adding the JAR to the tomcat.util.scan.DefaultJarScanner.jarsToSkip property in CATALINA_BASE/conf/catalina.properties file.
Oct 3, 2011 2:05:58 PM org.apache.jasper.compiler.TldLocationsCache tldScanJar
FINE: No TLD files were found in [file:/opt/tomcat/webapps/nestorurquiza-app/WEB-INF/lib/jsr250-api-1.0.jar]. Consider adding the JAR to the tomcat.util.scan.DefaultJarScanner.jarsToSkip property in CATALINA_BASE/conf/catalina.properties file.
Oct 3, 2011 2:05:58 PM org.apache.jasper.compiler.TldLocationsCache tldScanJar
FINE: No TLD files were found in [file:/opt/tomcat/webapps/nestorurquiza-app/WEB-INF/lib/org.springframework.security.ldap-3.0.5.RELEASE.jar]. Consider adding the JAR to the tomcat.util.scan.DefaultJarScanner.jarsToSkip property in CATALINA_BASE/conf/catalina.properties file.
...

Solution

Add all the project jars to the list in catalina.properties. I will need to see a real impact in performance because of this issue before spending time filling this list out in all of our Tomcat servers.

Tomcat 7 reveals log4j memory leak

We use MDC to log certain information in all traces

Tomcat 7 is notifying the following (As a difference with tomcat 6 which was silent):
SEVERE: The web application [/the-app] created a ThreadLocal with key of type [org.apache.log4j.helpers.ThreadLocalMap] (value [org.apache.log4j.helpers.ThreadLocalMap@757e5533]) and a value of type [java.util.Hashtable] (value [{sessionId=5366DB999B9EA1AC4CF30BED024BA44C, remoteAddress=127.0.0.1}]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak. 

Solution

Still waiting for a resolution on a Log4j memory leak: https://issues.apache.org/bugzilla/show_bug.cgi?id=50486

Tomcat 7 JSTL Failed to parse the expression

In Tomcat 7 (v7.0.22) method like isNew() cannot be referred as ${myObject.new} as before:
org.apache.jasper.JasperException: /WEB-INF/jsp/client/form.jsp (line: 5, column: 4) "${client.new}" contains invalid expression(s): javax.el.ELException: Failed to parse the expr
ession [${client.new}

This problem was documented a year ago and someone might be tempted to change the code for something like ${myObject.isNew()} after realizing that does work. However latest version of jasper-el breaks for this case which makes me think I will need to change my code again in future versions of Tomcat.

Solution

Change the code from ${client.new} to ${client['new']}

Alternative(s)

In mailing lists I understood Apache 7 is less permissive and since 'new' is not a valid Java identifier it cannot be part of the EL expression like in ${client.new}. There is flag to make Tomcat 7 more permissive albeit rewriting the code should be preferred:
-Dorg.apache.el.parser.SKIP_IDENTIFIER_CHECK=true

I came up with an alternative and temporary (and again not recommended) solution to avoid changing the code which is downloading latest jasper-el http://repo1.maven.org/maven2/org/apache/tomcat/jasper-el/6.0.33/jasper-el-6.0.33.jar or even copying the jar from a previous tomcat installation. Just remember to remove the old jar file:
$ cd ~
$ curl http://repo1.maven.org/maven2/org/apache/tomcat/jasper-el/6.0.33/jasper-el-6.0.33.jar > jasper-el-6.0.33.jar
$ mv /opt/apache-tomcat-7.0.22/lib/jasper-el.jar .
$ cp jasper-el-6.0.33.jar /opt/apache-tomcat-7.0.22/lib/
However that did not work for some other specific and more complex EL expressions (Not even 6.0.36 work with such more complex expressions. Here is just an example of one:
Internal Server Error org.apache.jasper.JasperException: /WEB-INF/jsp/workflow/processTaskInstance/list.jsp (line: 55, column: 20) "${serviceAgreementTypeNames.contains(processTaskInstanceDto.processDefinitionName)}" contains invalid expression(s): javax.el.ELException: Failed to parse the expression [${serviceAgreementTypeNames.contains(processTaskInstanceDto.processDefinitionName)}] at org.apache.jasper.compiler.DefaultErrorHandler.jspError(DefaultErrorHandler.java:42) at org.apache.jasper.compiler.ErrorDispatcher.dispatch(ErrorDispatcher.java:408) at org.apache.jasper.compiler.ErrorDispatcher.jspError(ErrorDispatcher.java:199) at org.apache.jasper.compiler.Validator$ValidateVisitor.checkXmlAttributes(Validator.java:1218) ...

Tomcat 7 TLD skipped ... is already defined

After deployment Tomcat 7 would log the below messages. Tomcat 6 did not:
INFO: TLD skipped. URI: http://java.sun.com/jstl/core_rt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/core is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jsp/jstl/core is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/fmt_rt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/fmt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jsp/jstl/fmt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jsp/jstl/functions is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://jakarta.apache.org/taglibs/standard/permittedTaglibs is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://jakarta.apache.org/taglibs/standard/scriptfree is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/sql_rt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/sql is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jsp/jstl/sql is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/xml_rt is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jstl/xml is already defined
Oct 3, 2011 11:45:44 AM org.apache.catalina.startup.TaglibUriRule body
INFO: TLD skipped. URI: http://java.sun.com/jsp/jstl/xml is already defined

Solution

Look for duplicates in the server/project jars. In my case spring JSTL has a dependency of Spring standard and eliminating the second solves the problem (The second includes the same TLDs again)
<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet.jsp.jstl</artifactId>
            <version>1.2.0</version>
            <exclusions>
             <exclusion>
              <artifactId>com.springsource.org.apache.taglibs.standard</artifactId>
              <groupId>org.apache.taglibs</groupId>
             </exclusion>
            </exclusions>
</dependency>

Saturday, October 01, 2011

Upgrade Ubuntu Apache to latest version in available repositories

We are using in some server Ubuntu 10.10 (maverick). It ships with Apache 2.2.14 and there is no repository with an upgrade for this highly compromised apache version.

The latest version of Ubuntu still in beta is 11.10 (Oneiric). It ships Apache 2.2.20 which includes important vulnerabilities fixes.

When you are in a situation like this you need to look for available debian repositories. A good place to search for them is http://repogen.simplylinux.ch/

From the site you will be able to obtain the sources.list file for any Ubuntu distro. Once you have the entries you need to add them locally and then run some commands.

So here is what you can do to upgrade Apache to 2.2.20 in Maverick (and probably other Ubuntu versions)
$ sudo vi /etc/apt/sources.list
...
deb http://us.archive.ubuntu.com/ubuntu/ oneiric main
...
$ sudo apt-get update
$ sudo apt-get install apache2
$ apache2 -v
Server version: Apache/2.2.20 (Ubuntu)
Server built:   Sep  6 2011 18:40:05
$ sudo vi /etc/apt/sources.list
...
#comment it out or delete it completely
#deb http://us.archive.ubuntu.com/ubuntu/ oneiric main
...
$ sudo apt-get update

Followers