Provide Last-Modified in responses

List Items and Attachments have modification times, but other types do
not.

We cannot use the date information for handling If-Modified-Since
because things like NoIndex and anonymous access depend on things in
other SP objects (like the parent Site).
diff --git a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
index 7ed279e..e90f18f 100644
--- a/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
+++ b/src/com/google/enterprise/adaptor/sharepoint/SharePointAdaptor.java
@@ -87,6 +87,9 @@
 import java.io.*;
 import java.net.*;
 import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.logging.*;
@@ -162,6 +165,9 @@
   private static final String CONTENTTYPEID_DOCUMENT_PREFIX = "0x0101";
   /** Provides the number of attachments the list item has. */
   private static final String OWS_ATTACHMENTS_ATTRIBUTE = "ows_Attachments";
+  /** The last time metadata or content was modified. */
+  private static final String OWS_LAST_MODIFIED_ATTRIBUTE
+      = "ows_Last_x0020_Modified";
   /**
    * Matches a SP-encoded value that contains one or more values. See {@link
    * SiteAdaptor.addMetadata}.
@@ -264,6 +270,28 @@
   private final Object refreshMemberIdMappingLock = new Object();
   
   private FormsAuthenticationHandler authenticationHandler;
+  private static final TimeZone gmt = TimeZone.getTimeZone("GMT");
+  /** RFC 822 date format, as updated by RFC 1123. */
+  private final ThreadLocal<DateFormat> dateFormatRfc1123
+      = new ThreadLocal<DateFormat>() {
+        @Override
+        protected DateFormat initialValue() {
+          DateFormat df = new SimpleDateFormat(
+              "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
+          df.setTimeZone(gmt);
+          return df;
+        }
+      };
+  private final ThreadLocal<DateFormat> modifiedDateFormat
+      = new ThreadLocal<DateFormat>() {
+        @Override
+        protected DateFormat initialValue() {
+          DateFormat df = new SimpleDateFormat(
+              "yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
+          df.setTimeZone(gmt);
+          return df;
+        }
+      };
 
   public SharePointAdaptor() {
     this(new SoapFactoryImpl(), new HttpClientImpl(),
@@ -1463,7 +1491,7 @@
       }
       response.addMetadata(METADATA_OBJECT_TYPE, "Aspx");      
       response.addMetadata(METADATA_PARENT_WEB_TITLE, w.webTitle);
-      getFileDocContent(request, response);
+      getFileDocContent(request, response, true);
       log.exiting("SiteAdaptor", "getAspxDocContent");
     }
 
@@ -1474,8 +1502,8 @@
      * ACLs and other metadata and security measures should be set before making
      * this call.
      */
-    private void getFileDocContent(Request request, Response response)
-        throws IOException {
+    private void getFileDocContent(Request request, Response response,
+        boolean setLastModified) throws IOException {
       log.entering("SiteAdaptor", "getFileDocContent",
           new Object[] {request, response});
       URI displayUrl = docIdToUri(request.getDocId());
@@ -1491,6 +1519,16 @@
         if (contentType != null) {
           response.setContentType(contentType);
         }
+        String lastModifiedString = fi.getFirstHeaderWithName("Last-Modified");
+        if (lastModifiedString != null && setLastModified) {
+          try {
+            response.setLastModified(
+                dateFormatRfc1123.get().parse(lastModifiedString));
+          } catch (ParseException ex) {
+            log.log(Level.INFO, "Could not parse Last-Modified: {0}",
+                lastModifiedString);
+          }
+        }
         IOHelper.copyStream(fi.getContents(), response.getOutputStream());
       } finally {
         fi.getContents().close();
@@ -1520,6 +1558,21 @@
       Element data = getFirstChildWithName(xml, DATA_ELEMENT);
       Element row = getChildrenWithName(data, ROW_ELEMENT).get(0);
 
+      String modifiedString = row.getAttribute(OWS_LAST_MODIFIED_ATTRIBUTE);
+      if (modifiedString == null) {
+        log.log(Level.FINE, "No last modified information for list item");
+      } else {
+        // This should be in the form of "1234;#DATE".
+        modifiedString = modifiedString.split(";#", 2)[1];
+        try {
+          response.setLastModified(
+              modifiedDateFormat.get().parse(modifiedString));
+        } catch (ParseException ex) {
+          log.log(Level.INFO, "Could not parse ows_Modified: {0}",
+              modifiedString);
+        }
+      }
+
       // This should be in the form of "1234;#{GUID}". We want to extract the
       // {GUID}.
       String scopeId
@@ -1700,7 +1753,7 @@
         // contents.
         metadataLength += addMetadata(
             response, METADATA_OBJECT_TYPE, "Document");
-        getFileDocContent(request, response);
+        getFileDocContent(request, response, false);
       } else {
         // Some list item.
         URI displayPage = sharePointUrlToUri(l.defaultViewItemUrl);
@@ -1806,7 +1859,7 @@
       response.addMetadata(METADATA_PARENT_WEB_TITLE, w.webTitle);
       response.addMetadata(METADATA_LIST_GUID, listId);
       // If the attachment doesn't exist, then this responds Not Found.
-      getFileDocContent(request, response);
+      getFileDocContent(request, response, true);
       log.exiting("SiteAdaptor", "getAttachmentDocContent", true);
       return true;
     }
diff --git a/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java b/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
index 6d622b9..192ce0d 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/GetContentsResponse.java
@@ -138,6 +138,10 @@
     return acl;
   }
 
+  public Date getLastModified() {
+    return lastModified;
+  }
+
   public List<URI> getAnchorUris() {
     return anchorUris;
   }
diff --git a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
index 5f2b977..53e3264 100644
--- a/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/sharepoint/SharePointAdaptorTest.java
@@ -798,7 +798,8 @@
         InputStream contents = new ByteArrayInputStream(
             goldenContents.getBytes(charset));
         List<String> headers = Arrays.asList("not-the-Content-Type", "early",
-            "conTent-TypE", goldenContentType, "Content-Type", "late");
+            "conTent-TypE", goldenContentType, "Content-Type", "late",
+            "Last-Modified", "Tue, 01 May 2012 22:14:41 GMT");
         return new FileInfo.Builder(contents).setHeaders(headers).build();
       }
     }, executorFactory);
@@ -826,6 +827,7 @@
           "http://localhost:1/sites/SiteCollection/Lists/Custom%20List/"
             + "Attachments/2/1046000.pdf"),
         response.getDisplayUrl());
+    assertEquals(new Date(1335910481000L), response.getLastModified());
   }
 
   @Test
@@ -921,6 +923,7 @@
     assertEquals(URI.create("http://localhost:1/sites/SiteCollection/Lists/"
           + "Custom%20List/DispForm.aspx?ID=2"),
         response.getDisplayUrl());
+    assertEquals(new Date(1335910446000L), response.getLastModified());
   }
 
   @Test