/*******************************************************************************
* Copyright (c) 2010, 2017 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.servlets.file;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.HashMap;
import javax.servlet.ServletContext;
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.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.provider.FileInfo;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.orion.internal.server.servlets.Activator;
import org.eclipse.orion.internal.server.servlets.ServletResourceHandler;
import org.eclipse.orion.server.core.EncodingUtils;
import org.eclipse.orion.server.core.IOUtilities;
import org.eclipse.orion.server.core.OrionConfiguration;
import org.eclipse.orion.server.core.ProtocolConstants;
import org.eclipse.orion.server.core.ServerStatus;
import org.eclipse.orion.server.core.metastore.WorkspaceInfo;
import org.eclipse.orion.server.servlets.OrionServlet;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.osgi.util.NLS;
import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.framework.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* General responder for IFileStore. This class provides general support for serializing
* and deserializing file and directory representations in server requests and responses.
* Specific behavior for a particular request is performed by a separate handler
* depending on file type and protocol version.
*/
public class ServletFileStoreHandler extends ServletResourceHandler<IFileStore> {
public static VersionRange VERSION1 = new VersionRange("[1,2)"); //$NON-NLS-1$
//The following two arrays must define their attributes in the same order
private static int[] ATTRIBUTE_BITS = new int[] {EFS.ATTRIBUTE_ARCHIVE, EFS.ATTRIBUTE_EXECUTABLE, EFS.ATTRIBUTE_HIDDEN, EFS.ATTRIBUTE_IMMUTABLE, EFS.ATTRIBUTE_READ_ONLY, EFS.ATTRIBUTE_SYMLINK};
private static String[] ATTRIBUTE_KEYS = new String[] {ProtocolConstants.KEY_ATTRIBUTE_ARCHIVE, ProtocolConstants.KEY_ATTRIBUTE_EXECUTABLE, ProtocolConstants.KEY_ATTRIBUTE_HIDDEN, ProtocolConstants.KEY_ATTRIBUTE_IMMUTABLE, ProtocolConstants.KEY_ATTRIBUTE_READ_ONLY, ProtocolConstants.KEY_ATTRIBUTE_SYMLINK};
private final ServletResourceHandler<IFileStore> directorySerializerV1;
private final ServletResourceHandler<IFileStore> fileSerializerV1;
private final ServletResourceHandler<IFileStore> genericDirectorySerializer;
private final ServletResourceHandler<IFileStore> genericFileSerializer;
final ServletResourceHandler<IStatus> statusHandler;
public static IFileInfo fromJSON(JSONObject object) {
FileInfo info = (FileInfo) EFS.createFileInfo();
copyJSONToFileInfo(object, info);
return info;
}
/**
* Copies any defined fields in the provided JSON object into the destination file info.
* @param source The JSON object to copy fields from
* @param destination The file info to copy fields to
*/
public static void copyJSONToFileInfo(JSONObject source, FileInfo destination) {
destination.setName(source.optString(ProtocolConstants.KEY_NAME, destination.getName()));
destination.setLastModified(source.optLong(ProtocolConstants.KEY_LAST_MODIFIED, destination.getLastModified()));
destination.setDirectory(source.optBoolean(ProtocolConstants.KEY_DIRECTORY, destination.isDirectory()));
JSONObject attributes = source.optJSONObject(ProtocolConstants.KEY_ATTRIBUTES);
if (attributes != null) {
for (int i = 0; i < ATTRIBUTE_KEYS.length; i++) {
//undefined means the client does not want to change the value, so can't interpret as false
if (!attributes.isNull(ATTRIBUTE_KEYS[i]))
destination.setAttribute(ATTRIBUTE_BITS[i], attributes.optBoolean(ATTRIBUTE_KEYS[i]));
}
}
}
public static IFileInfo fromJSON(HttpServletRequest request) throws IOException, JSONException {
return fromJSON(OrionServlet.readJSONRequest(request));
}
public static JSONObject toJSON(IFileStore store, IFileInfo info, URI location) {
JSONObject result = new JSONObject();
try {
result.put(ProtocolConstants.KEY_NAME, info.getName());
result.put(ProtocolConstants.KEY_LOCAL_TIMESTAMP, info.getLastModified());
if (location != null || info.isDirectory()) {
result.put(ProtocolConstants.KEY_DIRECTORY, info.isDirectory());
}
result.put(ProtocolConstants.KEY_LENGTH, info.getLength());
if (location != null) {
if (info.isDirectory() && !location.getPath().endsWith("/")) {
location = URIUtil.append(location, "");
}
result.put(ProtocolConstants.KEY_LOCATION, location);
try {
URI workspaceLocation = new URI(location.getScheme(), location.getAuthority(), Activator.LOCATION_WORKSPACE_SERVLET, null, location.getFragment());
workspaceLocation = URIUtil.append(workspaceLocation, new Path(location.getPath()).segment(1));
result.put(ProtocolConstants.KEY_WORKSPACE_LOCATION, workspaceLocation);
if (info.isDirectory())
result.put(ProtocolConstants.KEY_CHILDREN_LOCATION, new URI(location.getScheme(), location.getAuthority(), location.getPath(), "depth=1", location.getFragment())); //$NON-NLS-1$
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
JSONObject attributes = getAttributes(store, info, location == null);
if (attributes.keys().hasNext()) {
result.put(ProtocolConstants.KEY_ATTRIBUTES, attributes);
}
} catch (JSONException e) {
//cannot happen because the key is non-null and the values are strings
throw new RuntimeException(e);
}
return result;
}
/**
* Returns a JSON Object containing the attributes supported and defined by the given file.
*/
private static JSONObject getAttributes(IFileStore store, IFileInfo info, boolean optional) throws JSONException {
int supported = store.getFileSystem().attributes();
JSONObject attributes = new JSONObject();
for (int i = 0; i < ATTRIBUTE_KEYS.length; i++)
if ((supported & ATTRIBUTE_BITS[i]) != 0 && (!optional || info.getAttribute(ATTRIBUTE_BITS[i])))
attributes.put(ATTRIBUTE_KEYS[i], info.getAttribute(ATTRIBUTE_BITS[i]));
return attributes;
}
public ServletFileStoreHandler(ServletResourceHandler<IStatus> statusHandler, ServletContext context) {
this.statusHandler = statusHandler;
fileSerializerV1 = new FileHandlerV1(statusHandler, context);
genericFileSerializer = new GenericFileHandler(context);
directorySerializerV1 = new DirectoryHandlerV1(statusHandler);
genericDirectorySerializer = new GenericDirectoryHandler();
}
private boolean handleDirectory(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws ServletException {
String versionString = request.getHeader(ProtocolConstants.HEADER_ORION_VERSION);
Version version = versionString == null ? null : new Version(versionString);
ServletResourceHandler<IFileStore> handler;
if (version != null && VERSION1.isIncluded(version))
handler = directorySerializerV1;
else
handler = genericDirectorySerializer;
return handler.handleRequest(request, response, file);
}
private boolean handleFile(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws ServletException {
//could plug in more complex mapping here
String versionString = request.getHeader(ProtocolConstants.HEADER_ORION_VERSION);
Version version = versionString == null ? null : new Version(versionString);
ServletResourceHandler<IFileStore> handler;
if (version != null && VERSION1.isIncluded(version))
handler = fileSerializerV1;
else
handler = genericFileSerializer;
return handler.handleRequest(request, response, file);
}
public boolean handleRequest(HttpServletRequest request, HttpServletResponse response, IFileStore file) throws ServletException {
IFileInfo fileInfo;
try {
fileInfo = file.fetchInfo(EFS.NONE, null);
} catch (CoreException e) {
if (handleAuthFailure(request, response, e))
return true;
//assume file does not exist
fileInfo = new FileInfo(file.getName());
((FileInfo) fileInfo).setExists(false);
}
if (!request.getMethod().equals("PUT") && !fileInfo.exists()) { //$NON-NLS-1$
if("true".equals(request.getHeader("read-if-exists"))) {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.WARNING, 204, NLS.bind("No file content: {0}", EncodingUtils.encodeForHTML(request.getPathInfo())), null));
}
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.ERROR, 404, NLS.bind("File not found: {0}", EncodingUtils.encodeForHTML(request.getPathInfo())), null));
}
if("true".equals(IOUtilities.getQueryParameter(request, "project"))) {
return getProject(request, response, file, fileInfo);
}
if (fileInfo.isDirectory()) {
return handleDirectory(request, response, file);
}
return handleFile(request, response, file);
}
/**
* Tries to find the enclosing project context for the given file information
* @param request The original request
* @param fileInfo The file information to start looking from
* @param names The map of file names that are considered "project-like" files
* @return The name of the project containing the given file information
* @throws ServletException
* @since 14.0
*/
private boolean getProject(HttpServletRequest request, HttpServletResponse response, IFileStore file, IFileInfo fileInfo) throws ServletException {
String n = IOUtilities.getQueryParameter(request, "names");
HashMap<String, Boolean> names = new HashMap<String, Boolean>();
names.put(".git", true);
names.put("project.json", false);
if(n != null) {
try {
String[] clientNames = URLDecoder.decode(n, "UTF-8").split(",");
for (String _n : clientNames) {
names.put(_n, false);
}
} catch(UnsupportedEncodingException use) {
Logger logger = LoggerFactory.getLogger(ServletFileStoreHandler.class);
logger.error("Failed to decode client project names: " + n);
}
}
String pathString = request.getPathInfo();
IPath path = new Path(pathString);
String workspacePath = null;
String workspaceId = null;
try {
WorkspaceInfo workspace = OrionConfiguration.getMetaStore().readWorkspace(path.segment(0));
workspaceId = workspace.getUniqueId();
IFileStore userHome = OrionConfiguration.getMetaStore().getUserHome(workspace.getUserId());
IFileStore workspaceHome = userHome.getChild(workspaceId.substring(workspaceId.indexOf('-') + 1));
File workspaceRoot = workspaceHome.toLocalFile(EFS.NONE, null);
workspacePath = workspaceRoot.getAbsolutePath();
} catch (CoreException e) {
Logger logger = LoggerFactory.getLogger(ServletFileStoreHandler.class);
logger.error("Exception computing workspace path");
}
if(workspacePath == null) {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.WARNING, 204, NLS.bind("No workspace path for: {0}", EncodingUtils.encodeForHTML(fileInfo.getName())), null));
}
if(workspaceId == null) {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.WARNING, 204, NLS.bind("No workspace ID for: {0}", EncodingUtils.encodeForHTML(fileInfo.getName())), null));
}
IFileStore project = findProject(request, file, fileInfo, names, workspacePath);
if(project == null) {
return statusHandler.handleRequest(request, response, new ServerStatus(IStatus.WARNING, 204, NLS.bind("No project context for: {0}", EncodingUtils.encodeForHTML(fileInfo.getName())), null));
}
try {
URI location = getProjectURI(project.toURI(), workspacePath, workspaceId);
JSONObject result = toJSON(project, project.fetchInfo(), location);
OrionServlet.writeJSONResponse(request, response, result);
} catch (IOException ioe) {
Logger logger = LoggerFactory.getLogger(ServletFileStoreHandler.class);
logger.error("Failed to write response for project: " + project.getName());
}
return true;
}
/**
* Resolve the correct project URI using the <code>orion</code> scheme
* @param projectUri The computed project URI
* @return The {@link URI} to pass back in the response
* @since 14.0
*/
private URI getProjectURI(URI projectUri, String workspacePath, String workspaceId) {
String pathInfo = projectUri.getPath().substring(workspacePath.length());
try {
// Note: no query string!
return new URI("orion", null, "/file/"+workspaceId+pathInfo, null, null);
} catch (URISyntaxException e) {
//location not properly encoded
return null;
}
}
/**
* @param fileInfo The originating fileInfo
* @param file The originating file store
* @param request The original request
* @param names The map of names to consider project-like
* @return The {@link IFileStore} for the project or <code>null</code>
* @since 14.0
*/
private IFileStore findProject(HttpServletRequest request, IFileStore file, IFileInfo fileInfo, HashMap<String, Boolean> names, String workspacePath) {
IFileStore parent = file;
try {
File parentFile = parent.toLocalFile(EFS.NONE, null);
if (parentFile == null) {
Logger logger = LoggerFactory.getLogger(ServletFileStoreHandler.class);
logger.error("Unable to get the parent local file from: " + parent);
return null;
}
while(parent != null && parentFile != null && parentFile.getAbsolutePath().startsWith(workspacePath)) {
IFileInfo[] children = parent.childInfos(EFS.NONE, null);
for (IFileInfo childInfo : children) {
if(childInfo.exists() && names.containsKey(childInfo.getName())) {
if(childInfo.isDirectory() && names.get(childInfo.getName())) {
return parent;
} else if(!names.get(childInfo.getName())) {
return parent;
}
}
}
parent = parent.getParent();
parentFile = parent.toLocalFile(EFS.NONE, null);
}
} catch (CoreException ce) {
Logger logger = LoggerFactory.getLogger(ServletFileStoreHandler.class);
logger.error("Exception computing parent folder from: " + parent);
return null;
}
return null;
}
}