summaryrefslogtreecommitdiffstatsabout
diff options
context:
space:
mode:
authorPiotr Janik2011-07-05 06:44:18 (EDT)
committer Szymon Brandys2011-07-05 06:44:18 (EDT)
commit921c3dbcb1db513af5a227791d62f31892c4c4ac (patch)
tree6aa7613b2df562a6735cce92ef8281b71eb0fb7e
parent0f7e4749fe036c0cf553890c304be94f1a0269e6 (diff)
downloadorg.eclipse.orion.server-921c3dbcb1db513af5a227791d62f31892c4c4ac.zip
org.eclipse.orion.server-921c3dbcb1db513af5a227791d62f31892c4c4ac.tar.gz
org.eclipse.orion.server-921c3dbcb1db513af5a227791d62f31892c4c4ac.tar.bz2
bug 350410 - File content overridden when saved from multiple editors
https://bugs.eclipse.org/bugs/show_bug.cgi?id=350410
-rw-r--r--bundles/org.eclipse.orion.server.core/src/org/eclipse/orion/internal/server/core/HashUtilities.java87
-rw-r--r--bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/ProtocolConstants.java6
-rw-r--r--bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/FileHandlerV1.java38
-rw-r--r--bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/GenericFileHandler.java18
-rw-r--r--tests/org.eclipse.orion.server.tests/src/org/eclipse/orion/server/tests/servlets/files/AdvancedFilesTest.java54
5 files changed, 191 insertions, 12 deletions
diff --git a/bundles/org.eclipse.orion.server.core/src/org/eclipse/orion/internal/server/core/HashUtilities.java b/bundles/org.eclipse.orion.server.core/src/org/eclipse/orion/internal/server/core/HashUtilities.java
new file mode 100644
index 0000000..a839b7c
--- /dev/null
+++ b/bundles/org.eclipse.orion.server.core/src/org/eclipse/orion/internal/server/core/HashUtilities.java
@@ -0,0 +1,87 @@
+/*******************************************************************************
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.orion.internal.server.core;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Various static helper methods for hash calculations.
+ */
+public class HashUtilities {
+ public static final String SHA_1 = "SHA-1";
+
+ /**
+ * Returns the hash of data read from the given input stream.
+ * @param inputStream input stream
+ * @param hashFunction desired hash function (i.e. SHA-1)
+ * @return text representation of the hash
+ * @throws IOException
+ * @throws NoSuchAlgorithmException
+ */
+ public static String getHash(InputStream inputStream, String hashFunction) throws IOException, NoSuchAlgorithmException {
+ return getHash(inputStream, false, hashFunction);
+ }
+
+ /**
+ * Returns the hash of data read from the given input stream.
+ * @param inputStream input stream
+ * @param hashFunction desired hash function (i.e. SHA-1)
+ * @param closeIn determines if input stream should be closed after reading
+ * @return text representation of the hash
+ * @throws IOException
+ * @throws NoSuchAlgorithmException
+ */
+ public static String getHash(InputStream inputStream, boolean closeIn, String hashFunction) throws IOException, NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance(hashFunction);
+
+ byte[] buffer = new byte[4096];
+ int read = 0;
+ try {
+ while ((read = inputStream.read(buffer)) != -1)
+ md.update(buffer, 0, read);
+ } finally {
+ if (closeIn)
+ IOUtilities.safeClose(inputStream);
+ }
+
+ byte[] mdbytes = md.digest();
+
+ return bytesToHex(mdbytes);
+ }
+
+ /**
+ * Returns the hash of the given input String.
+ * @param inputStream input stream
+ * @param hashFunction desired hash function (i.e. SHA-1)
+ * @return text representation of the hash
+ * @throws NoSuchAlgorithmException
+ */
+ public static String getHash(String data, String hashFunction) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance(hashFunction);
+
+ md.update(data.getBytes());
+ byte[] mdbytes = md.digest();
+
+ return bytesToHex(mdbytes);
+ }
+
+ //convert the byte to hex format
+ private static String bytesToHex(byte[] bytes) {
+ StringBuffer sb = new StringBuffer("");
+ for (int i = 0; i < bytes.length; i++) {
+ sb.append(Integer.toHexString(0xFF & bytes[i]));
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/ProtocolConstants.java b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/ProtocolConstants.java
index 2cff813..feebe7f 100644
--- a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/ProtocolConstants.java
+++ b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/ProtocolConstants.java
@@ -285,6 +285,12 @@ public class ProtocolConstants {
public static final String KEY_ATTRIBUTE_SYMLINK = "SymLink"; //$NON-NLS-1$
/**
+ * ETag is an opaque identifier assigned to a specific version of a resource found at a URI.
+ * If the resource content at that URL ever changes, a new and different ETag is assigned.
+ */
+ public static final String KEY_ETAG = "ETag"; //$NON-NLS-1$
+
+ /**
* JSON representation key for a server host attribute. The value's data
* type is a String.
*/
diff --git a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/FileHandlerV1.java b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/FileHandlerV1.java
index 8b7490b..581c630 100644
--- a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/FileHandlerV1.java
+++ b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/FileHandlerV1.java
@@ -11,6 +11,7 @@
package org.eclipse.orion.internal.server.servlets.file;
import java.io.*;
+import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
@@ -46,20 +47,25 @@ class FileHandlerV1 extends GenericFileHandler {
*/
private static final String EOL = "\r\n"; //$NON-NLS-1$
- protected void handleGetMetadata(HttpServletRequest request, Writer response, IFileStore file) throws IOException {
+ // responseWriter is used, as in some cases response should be
+ // appended to response generated earlier (i.e. multipart get)
+ protected void handleGetMetadata(HttpServletRequest request, HttpServletResponse response, Writer responseWriter, IFileStore file) throws IOException, NoSuchAlgorithmException, JSONException, CoreException {
JSONObject result = ServletFileStoreHandler.toJSON(file, file.fetchInfo(), getURI(request));
+ String etag = generateFileETag(file);
+ result.put(ProtocolConstants.KEY_ETAG, etag);
+ response.setHeader(ProtocolConstants.KEY_ETAG, etag);
OrionServlet.decorateResponse(request, result);
- response.append(result.toString());
+ responseWriter.append(result.toString());
}
- private void handleMultiPartGet(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws IOException, CoreException {
+ private void handleMultiPartGet(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws IOException, CoreException, NoSuchAlgorithmException, JSONException {
String boundary = createBoundaryString();
response.setHeader(ProtocolConstants.HEADER_CONTENT_TYPE, "multipart/related; boundary=\"" + boundary + '"'); //$NON-NLS-1$
OutputStream outputStream = response.getOutputStream();
Writer out = new OutputStreamWriter(outputStream);
out.write("--" + boundary + EOL); //$NON-NLS-1$
out.write("Content-Type: application/json" + EOL + EOL); //$NON-NLS-1$
- handleGetMetadata(request, out, file);
+ handleGetMetadata(request, response, out, file);
out.write(EOL + "--" + boundary + EOL); //$NON-NLS-1$
// headers for file contents go here
out.write(EOL);
@@ -73,7 +79,15 @@ class FileHandlerV1 extends GenericFileHandler {
return new UniversalUniqueIdentifier().toBase64String();
}
- private void handleMultiPartPut(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws IOException, CoreException, JSONException {
+ private void handlePutContents(HttpServletRequest request, BufferedReader requestReader, HttpServletResponse response, IFileStore file) throws IOException, CoreException, NoSuchAlgorithmException, JSONException {
+ Writer fileWriter = new BufferedWriter(new OutputStreamWriter(file.openOutputStream(EFS.NONE, null)));
+ IOUtilities.pipe(requestReader, fileWriter, false, true);
+
+ // return metadata with the new Etag
+ handleGetMetadata(request, response, response.getWriter(), file);
+ }
+
+ private void handleMultiPartPut(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws IOException, CoreException, JSONException, NoSuchAlgorithmException {
String typeHeader = request.getHeader(ProtocolConstants.HEADER_CONTENT_TYPE);
String boundary = typeHeader.substring(typeHeader.indexOf("boundary=\"") + 10, typeHeader.length() - 1); //$NON-NLS-1$
BufferedReader requestReader = request.getReader();
@@ -87,8 +101,7 @@ class FileHandlerV1 extends GenericFileHandler {
contentHeaders.put(header[0], header[1]);
}
// now for the file contents
- Writer fileWriter = new BufferedWriter(new OutputStreamWriter(file.openOutputStream(EFS.NONE, null)));
- IOUtilities.pipe(requestReader, fileWriter, false, true);
+ handlePutContents(request, requestReader, response, file);
}
private void handlePutMetadata(BufferedReader reader, String boundary, IFileStore file) throws IOException, CoreException, JSONException {
@@ -105,12 +118,20 @@ class FileHandlerV1 extends GenericFileHandler {
@Override
public boolean handleRequest(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws ServletException {
try {
+ String receivedETag = request.getHeader("If-Match");
+ if (receivedETag != null && !receivedETag.equals(generateFileETag(file))) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return true;
+ }
String parts = IOUtilities.getQueryParameter(request, "parts");
if (parts == null || "body".equals(parts)) { //$NON-NLS-1$
switch (getMethod(request)) {
case DELETE :
file.delete(EFS.NONE, null);
break;
+ case PUT :
+ handlePutContents(request, request.getReader(), response, file);
+ break;
default :
handleFileContents(request, response, file);
}
@@ -119,7 +140,7 @@ class FileHandlerV1 extends GenericFileHandler {
if ("meta".equals(parts)) { //$NON-NLS-1$
switch (getMethod(request)) {
case GET :
- handleGetMetadata(request, response.getWriter(), file);
+ handleGetMetadata(request, response, response.getWriter(), file);
return true;
case PUT :
handlePutMetadata(request.getReader(), null, file);
@@ -135,7 +156,6 @@ class FileHandlerV1 extends GenericFileHandler {
return true;
case PUT :
handleMultiPartPut(request, response, file);
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return true;
}
return false;
diff --git a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/GenericFileHandler.java b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/GenericFileHandler.java
index 1f37d5a..70dca31 100644
--- a/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/GenericFileHandler.java
+++ b/bundles/org.eclipse.orion.server.servlets/src/org/eclipse/orion/internal/server/servlets/file/GenericFileHandler.java
@@ -11,13 +11,16 @@
package org.eclipse.orion.internal.server.servlets.file;
import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
+import org.eclipse.orion.internal.server.core.HashUtilities;
import org.eclipse.orion.internal.server.core.IOUtilities;
+import org.eclipse.orion.internal.server.servlets.ProtocolConstants;
import org.eclipse.orion.internal.server.servlets.ServletResourceHandler;
import org.eclipse.osgi.util.NLS;
@@ -26,7 +29,12 @@ import org.eclipse.osgi.util.NLS;
* such as a web browser.
*/
class GenericFileHandler extends ServletResourceHandler<IFileStore> {
- protected void handleFileContents(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws CoreException, IOException {
+ protected void handleFileContents(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws CoreException, IOException, NoSuchAlgorithmException {
+ String receivedETag = request.getHeader("If-Match");
+ if (receivedETag != null && !receivedETag.equals(generateFileETag(file))) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
+ return;
+ }
switch (getMethod(request)) {
case GET :
IOUtilities.pipe(file.openInputStream(EFS.NONE, null), response.getOutputStream(), true, false);
@@ -36,6 +44,7 @@ class GenericFileHandler extends ServletResourceHandler<IFileStore> {
break;
}
response.setHeader("Cache-Control", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
+ response.setHeader(ProtocolConstants.KEY_ETAG, generateFileETag(file));
}
@Override
@@ -51,4 +60,11 @@ class GenericFileHandler extends ServletResourceHandler<IFileStore> {
}
return true;
}
+
+ /**
+ * Returns an ETag calculated using SHA-1 hash function.
+ */
+ public static String generateFileETag(IFileStore file) throws NoSuchAlgorithmException, IOException, CoreException {
+ return HashUtilities.getHash(Long.toString(file.fetchInfo().getLastModified()), HashUtilities.SHA_1);
+ }
} \ No newline at end of file
diff --git a/tests/org.eclipse.orion.server.tests/src/org/eclipse/orion/server/tests/servlets/files/AdvancedFilesTest.java b/tests/org.eclipse.orion.server.tests/src/org/eclipse/orion/server/tests/servlets/files/AdvancedFilesTest.java
index 34b98b4..8599941 100644
--- a/tests/org.eclipse.orion.server.tests/src/org/eclipse/orion/server/tests/servlets/files/AdvancedFilesTest.java
+++ b/tests/org.eclipse.orion.server.tests/src/org/eclipse/orion/server/tests/servlets/files/AdvancedFilesTest.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2010 IBM Corporation and others
+ * Copyright (c) 2010, 2011 IBM Corporation and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -11,21 +11,27 @@
package org.eclipse.orion.server.tests.servlets.files;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
-import com.meterware.httpunit.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
+
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.runtime.CoreException;
+import org.eclipse.orion.internal.server.servlets.ProtocolConstants;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.SAXException;
+import com.meterware.httpunit.WebConversation;
+import com.meterware.httpunit.WebRequest;
+import com.meterware.httpunit.WebResponse;
+
public class AdvancedFilesTest extends FileSystemTest {
@Before
public void prepereEmptyEnviroment() throws CoreException {
@@ -95,4 +101,48 @@ public class AdvancedFilesTest extends FileSystemTest {
}
+ @Test
+ public void testETagHandling() throws JSONException, IOException, SAXException {
+ String fileName = "testfile.txt";
+
+ //setup: create a file
+ WebConversation webConversation = new WebConversation();
+ webConversation.setExceptionsThrownOnErrorStatus(false);
+ WebRequest request = getPostFilesRequest("/", getNewFileJSON(fileName).toString(), fileName);
+ WebResponse response = webConversation.getResponse(request);
+ assertEquals(HttpURLConnection.HTTP_CREATED, response.getResponseCode());
+
+ //obtain file metadata and ensure data is correct
+ request = getGetFilesRequest(fileName + "?parts=meta");
+ response = webConversation.getResponse(request);
+ assertEquals(HttpURLConnection.HTTP_OK, response.getResponseCode());
+ JSONObject responseObject = new JSONObject(response.getText());
+ assertNotNull("No file information in response", responseObject);
+
+ String etag1 = responseObject.getString(ProtocolConstants.KEY_ETAG);
+ assertEquals(etag1, response.getHeaderField(ProtocolConstants.KEY_ETAG));
+
+ //modify file
+ request = getPutFileRequest(fileName, "something");
+ response = webConversation.getResponse(request);
+ assertEquals(HttpURLConnection.HTTP_OK, response.getResponseCode());
+ responseObject = new JSONObject(response.getText());
+ assertNotNull("No file information in response", responseObject);
+
+ String etag2 = responseObject.getString(ProtocolConstants.KEY_ETAG);
+ assertEquals(etag2, response.getHeaderField(ProtocolConstants.KEY_ETAG));
+ // should be different as file was modified
+ assertFalse(etag2.equals(etag1));
+
+ //fetch the metadata again and ensure it is changed and correct
+ request = getGetFilesRequest(fileName + "?parts=meta");
+ response = webConversation.getResponse(request);
+ assertEquals(HttpURLConnection.HTTP_OK, response.getResponseCode());
+ responseObject = new JSONObject(response.getText());
+
+ String etag3 = responseObject.getString(ProtocolConstants.KEY_ETAG);
+ assertEquals(etag3, response.getHeaderField(ProtocolConstants.KEY_ETAG));
+ assertEquals(etag2, etag3);
+ }
+
}