Index: solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java
===================================================================
--- solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java	(revision 1408341)
+++ solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java	(working copy)
@@ -25,8 +25,11 @@
 import org.apache.solr.common.params.TermsParams;
 import org.apache.solr.common.util.DateUtil;
 
+import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.Locale;
+import java.util.Map;
 import java.util.regex.Pattern;
 
 
@@ -44,6 +47,9 @@
       return (this == asc) ? desc : asc;
     }
   }
+
+  /** Maintains a map of current sorts */
+  private LinkedHashMap<String,ORDER> sortMap;
   
   public SolrQuery() {
     super();
@@ -529,38 +535,158 @@
     return this.get(HighlightParams.SIMPLE_POST, "");
   }
 
+  @Deprecated
   public SolrQuery setSortField(String field, ORDER order) {
     this.remove(CommonParams.SORT);
     addValueToParam(CommonParams.SORT, toSortString(field, order));
     return this;
   }
   
+  @Deprecated
   public SolrQuery addSortField(String field, ORDER order) {
     return addValueToParam(CommonParams.SORT, toSortString(field, order));
   }
 
+  @Deprecated
   public SolrQuery removeSortField(String field, ORDER order) {
-    String s = this.get(CommonParams.SORT);
-    String removeSort = toSortString(field, order);
-    if (s != null) {
-      String[] sorts = s.split(",");
-      s = join(sorts, ", ", removeSort);
+    String[] sorts = getSortFields();
+    if (sorts != null) {
+      String removeSort = toSortString(field, order);
+      String s = join(sorts, ",", removeSort);
       if (s.length()==0) s=null;
       this.set(CommonParams.SORT, s);
     }
     return this;
   }
   
+  @Deprecated
   public String[] getSortFields() {
     String s = getSortField();
     if (s==null) return null;
-    return s.split(",");
+    return s.trim().split(", *");
   }
 
+  @Deprecated
   public String getSortField() {
     return this.get(CommonParams.SORT);
   }
   
+  /**
+   * Replaces the current sort information. The map is 
+   * indexed by field name, with each value containing the 
+   * {@link SolrQuery.ORDER} of that field.
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery setSort(Map<String, ORDER> value) {
+    this.sortMap = new LinkedHashMap<String, ORDER>(value);
+    serializeSort();
+    return this;
+  }
+
+  /**
+   * Clears current sort information.
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery clearSort() {
+    this.sortMap = null;
+    serializeSort();
+    return this;
+  }
+
+  /**
+   * Gets an ordered map representing the current sort fields. The map is 
+   * indexed by field name, with each value containing the 
+   * {@link SolrQuery.ORDER} of that field.
+   * @return an ordered, immutable map of current sort fields 
+   * @since 4.1
+   */
+  public Map<String, ORDER> getSort() {
+    if (sortMap == null)
+      return Collections.emptyMap();
+    else
+      return Collections.unmodifiableMap(sortMap);
+  }
+
+  /**
+   * Replaces the current sort information with a single sort
+   * field specification.
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery setSort(String field, ORDER order) {
+    this.sortMap = new LinkedHashMap<String, ORDER>();
+    sortMap.put(field,  order);
+    serializeSort();
+    return this;
+  }
+  
+  /**
+   * Adds a single sort field specification to the end of the
+   * current sort information. If the sort field already exists
+   * in the sort information map, it is moved to the end.
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery addSort(String field, ORDER order) {
+    if (this.sortMap == null)
+      sortMap = new LinkedHashMap<String,ORDER>();
+    sortMap.put(field, order);
+    serializeSort();
+    return this;
+  }
+
+  /**
+   * Updates or adds a single sort field specification to the
+   * current sort information. If the sort field already exist
+   * in the sort information map, it's position is unchanged
+   * and the sort order is set; if it does not exist, it is
+   * appended at the end with the specified order..
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery addOrUpdateSort(String field, ORDER order) {
+    if (this.sortMap == null)
+      sortMap = new LinkedHashMap<String,ORDER>();
+    sortMap.put(field, order);
+    serializeSort();
+    return this;
+  }
+
+  /**
+   * Removes a single sort field from the current sort information.
+   * @return the modified SolrQuery object, for easy chaining 
+   * @since 4.1
+   */
+  public SolrQuery removeSort(String field) {
+    if (this.sortMap != null)  {
+      this.sortMap.remove (field);
+      if (this.sortMap.isEmpty())
+        this.sortMap = null;
+      serializeSort();
+    }
+    return this;
+  }
+
+  private void serializeSort() {
+    Map<String,ORDER> map = getSort();
+    if (map.isEmpty()) {
+      remove(CommonParams.SORT);
+    }
+    else {
+      StringBuilder sb = new StringBuilder();
+      for (Map.Entry<String, ORDER> entry : getSort().entrySet()) {
+        if (sb.length() > 0)
+          sb.append(",");
+        sb.append(entry.getKey());
+        sb.append(" ");
+        sb.append(entry.getValue());
+      }
+      set(CommonParams.SORT, sb.toString());
+    }
+  }
+  
   public void setGetFieldStatistics( boolean v )
   {
     this.set( StatsParams.STATS, v );
@@ -823,11 +949,11 @@
   private String join(String[] vals, String sep, String removeVal) {
     StringBuilder sb = new StringBuilder();
     for (int i=0; i<vals.length; i++) {
-      if (removeVal==null || !vals[i].equals(removeVal)) {
+      if (!vals[i].equals(removeVal)) {
+        if (sb.length() > 0) {
+          sb.append(sep);          
+        }
         sb.append(vals[i]);
-        if (i<vals.length-1) {
-          sb.append(sep);
-        }
       }
     }
     return sb.toString().trim();
Index: solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java
===================================================================
--- solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java	(revision 1408341)
+++ solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java	(working copy)
@@ -18,11 +18,13 @@
 package org.apache.solr.client.solrj;
 
 import org.apache.lucene.util.LuceneTestCase;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.FacetParams;
 
 import junit.framework.Assert;
 import org.apache.solr.common.util.DateUtil;
 
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
@@ -97,7 +99,118 @@
     
     // System.out.println(q);
   }
+
+  /*
+   * Verifies that the old (deprecated) sort methods
+   * allows mix-and-match between the raw field and
+   * the itemized apis.
+   */
+  public void testSortFieldRawStringAndMethods() {
+    SolrQuery q = new SolrQuery("dog");
+    q.set("sort", "price asc,date desc,qty desc");
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+    q.set("sort", "price asc, date desc, qty desc");
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+  }
   
+  /*
+   *  Verifies that you can use removeSortField() twice, which
+   *  did not work in 4.0
+   */
+  public void testSortFieldRemoveAfterRemove() {
+    SolrQuery q = new SolrQuery("dog");
+    q.addSortField("price", SolrQuery.ORDER.asc);
+    q.addSortField("date", SolrQuery.ORDER.desc);
+    q.addSortField("qty", SolrQuery.ORDER.desc);
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+    q.removeSortField("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals(1, q.getSortFields().length);
+  }
+  
+  /*
+   * Verifies that you can remove the last sort field, which
+   * did not work in 4.0
+   */
+  public void testSortFieldRemoveLast() {
+    SolrQuery q = new SolrQuery("dog");
+    q.addSortField("date", SolrQuery.ORDER.desc);
+    q.addSortField("qty", SolrQuery.ORDER.desc);
+    q.removeSortField("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals("date desc", q.getSortField());
+  }
+
+  /*
+   * Verifies that getSort() returns an immutable map,
+   * for both empty and non-empty situations
+   */
+  public void testGetSortImmutable() {
+    SolrQuery q = new SolrQuery("dog");
+
+    try {
+      q.getSort().put("price",  SolrQuery.ORDER.asc);
+      fail("The returned (empty) map should be immutable; put() should fail!");
+    } catch (UnsupportedOperationException uoe) {
+      // pass
+    }
+
+    q.addSortField("qty", SolrQuery.ORDER.desc);
+    try {
+      q.getSort().put("price",  SolrQuery.ORDER.asc);
+      fail("The returned (non-empty) map should be immutable; put() should fail!");
+    } catch (UnsupportedOperationException uoe) {
+      // pass
+    }
+  }
+
+  /*
+   * Verifies the symbolic sort operations
+   */
+  public void testSort() {
+    
+    SolrQuery q = new SolrQuery("dog");
+    
+    // Simple adds
+    q.addSort("price", SolrQuery.ORDER.asc);
+    q.addSort("date", SolrQuery.ORDER.desc);
+    q.addSort("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals(3, q.getSort().size());
+    Assert.assertEquals("[price, date, qty]", q.getSort().keySet().toString());
+    Assert.assertEquals("[asc, desc, desc]", q.getSort().values().toString());
+    Assert.assertEquals("price asc,date desc,qty desc", q.get(CommonParams.SORT));
+
+    // Remove one (middle)
+    q.removeSort("date");
+    Assert.assertEquals(2, q.getSort().size());
+    Assert.assertEquals("[price, qty]", q.getSort().keySet().toString());
+    Assert.assertEquals("[asc, desc]", q.getSort().values().toString());
+    Assert.assertEquals("price asc,qty desc", q.get(CommonParams.SORT));
+    
+    // Remove remaining (last, first)
+    q.removeSort("price");
+    q.removeSort("qty");
+    Assert.assertTrue(q.getSort().isEmpty());
+    Assert.assertNull(q.get(CommonParams.SORT));
+
+    // Clear sort
+    q.addSort("price", SolrQuery.ORDER.asc);
+    q.clearSort();
+    Assert.assertTrue(q.getSort().isEmpty());
+    Assert.assertNull(q.get(CommonParams.SORT));
+    
+    // Add vs update
+    q.clearSort();
+    q.addSort("1", SolrQuery.ORDER.asc);
+    q.addSort("2", SolrQuery.ORDER.asc);
+    q.addSort("3", SolrQuery.ORDER.asc);
+    q.addOrUpdateSort("2", SolrQuery.ORDER.desc);
+    q.addOrUpdateSort("4", SolrQuery.ORDER.desc);
+    Assert.assertEquals("[1, 2, 3, 4]", q.getSort().keySet().toString());
+    Assert.assertEquals("[asc, desc, asc, desc]", q.getSort().values().toString());
+  }
+
   public void testFacetSort() {
     SolrQuery q = new SolrQuery("dog");
     assertEquals("count", q.getFacetSortString());
